Skip to content

Commit f2ceb57

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Include HTML and CSS content in trace export
For enhanced traces with script contents, inline scripts within HTML document did not show up correctly in the Sources panel. Only the last inline script showed there, which is confusing. Now, we export the contents of all HTML documents. To accommodate this, the "script contents" checkbox is renamed to "resource contents". Also now including CSS resources. Fixed: 455942736 Change-Id: I44e7309d1df16f388307cfc20ea74c0cc8fd9fe1 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7088202 Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent 0334a50 commit f2ceb57

File tree

13 files changed

+326
-122
lines changed

13 files changed

+326
-122
lines changed

front_end/core/sdk/EnhancedTracesParser.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type * as Platform from '../platform/platform.js';
88
import {UserVisibleError} from '../platform/platform.js';
99

1010
import type {
11-
HydratingDataPerTarget, RehydratingExecutionContext, RehydratingScript, RehydratingTarget} from
11+
HydratingDataPerTarget, RehydratingExecutionContext, RehydratingResource, RehydratingScript, RehydratingTarget} from
1212
'./RehydratingObject.js';
1313
import type {SourceMapV3} from './SourceMap.js';
1414
import type {TraceObject} from './TraceObject.js';
@@ -142,6 +142,7 @@ export class EnhancedTracesParser {
142142
#targets: RehydratingTarget[] = [];
143143
#executionContexts: RehydratingExecutionContext[] = [];
144144
#scripts: RehydratingScript[] = [];
145+
#resources: RehydratingResource[] = [];
145146
static readonly enhancedTraceVersion: number = 1;
146147

147148
constructor(trace: TraceObject) {
@@ -319,7 +320,10 @@ export class EnhancedTracesParser {
319320
this.resolveSourceMap(script);
320321
}
321322

322-
return this.groupContextsAndScriptsUnderTarget(this.#targets, this.#executionContexts, this.#scripts);
323+
this.#resources = this.#trace.metadata.resources ?? [];
324+
325+
return this.groupContextsAndScriptsUnderTarget(
326+
this.#targets, this.#executionContexts, this.#scripts, this.#resources);
323327
}
324328

325329
private resolveSourceMap(script: RehydratingScript): void {
@@ -411,8 +415,8 @@ export class EnhancedTracesParser {
411415
}
412416

413417
private groupContextsAndScriptsUnderTarget(
414-
targets: RehydratingTarget[], executionContexts: RehydratingExecutionContext[],
415-
scripts: RehydratingScript[]): HydratingDataPerTarget[] {
418+
targets: RehydratingTarget[], executionContexts: RehydratingExecutionContext[], scripts: RehydratingScript[],
419+
resources: RehydratingResource[]): HydratingDataPerTarget[] {
416420
const data: HydratingDataPerTarget[] = [];
417421
const targetIds = new Set<Protocol.Target.TargetID>();
418422
const targetToExecutionContexts: Map<string, RehydratingExecutionContext[]> =
@@ -424,12 +428,15 @@ export class EnhancedTracesParser {
424428
const targetToScripts: Map<Protocol.Target.TargetID, RehydratingScript[]> =
425429
new Map<Protocol.Target.TargetID, RehydratingScript[]>();
426430
const orphanScripts: RehydratingScript[] = [];
431+
const targetToResources: Map<Protocol.Target.TargetID, RehydratingResource[]> =
432+
new Map<Protocol.Target.TargetID, RehydratingResource[]>();
427433

428434
// Initialize all the mapping needed
429435
for (const target of targets) {
430436
targetIds.add(target.targetId);
431437
targetToExecutionContexts.set(target.targetId, []);
432438
targetToScripts.set(target.targetId, []);
439+
targetToResources.set(target.targetId, []);
433440
}
434441

435442
// Put all of the known execution contexts under respective targets
@@ -485,12 +492,20 @@ export class EnhancedTracesParser {
485492
}
486493
}
487494

495+
for (const resource of resources) {
496+
const frameId = resource.frame as Protocol.Target.TargetID;
497+
if (targetIds.has(frameId)) {
498+
targetToResources.get(frameId)?.push(resource);
499+
}
500+
}
501+
488502
// Now all the scripts are linked to a target, we want to make sure all the scripts are pointing to a valid
489503
// execution context. If not, we will create an artificial execution context for the script
490504
for (const target of targets) {
491505
const targetId = target.targetId;
492506
const executionContexts = targetToExecutionContexts.get(targetId) || [];
493507
const scripts = targetToScripts.get(targetId) || [];
508+
const resources = targetToResources.get(targetId) || [];
494509
for (const script of scripts) {
495510
if (!executionContexts.find(context => context.id === script.executionContextId)) {
496511
const artificialContext: RehydratingExecutionContext = {
@@ -511,7 +526,7 @@ export class EnhancedTracesParser {
511526
}
512527

513528
// Finally, we put all the information into the data structure we want to return as.
514-
data.push({target, executionContexts, scripts});
529+
data.push({target, executionContexts, scripts, resources});
515530
}
516531

517532
return data;

front_end/core/sdk/RehydratingConnection.test.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import * as Common from '../common/common.js';
1010
import type {Message} from '../protocol_client/InspectorBackend.js';
1111

1212
import type {
13-
RehydratingExecutionContext, RehydratingScript, RehydratingTarget, ServerMessage} from './RehydratingObject.js';
13+
RehydratingExecutionContext, RehydratingResource, RehydratingScript, RehydratingTarget, ServerMessage} from
14+
'./RehydratingObject.js';
1415
import * as SDK from './sdk.js';
1516

1617
const mockTarget1: RehydratingTarget = {
@@ -60,7 +61,7 @@ const mockScript1: RehydratingScript = {
6061
endColumn: 10,
6162
hash: '',
6263
isModule: false,
63-
url: 'example.com',
64+
url: 'example.com', // inline
6465
hasSourceURL: false,
6566
sourceMapURL: undefined,
6667
length: 10,
@@ -84,7 +85,7 @@ const mockScript2: RehydratingScript = {
8485
endColumn: 10,
8586
hash: '',
8687
isModule: false,
87-
url: 'example.com',
88+
url: 'example.com/script.js',
8889
hasSourceURL: false,
8990
sourceMapURL: undefined,
9091
length: 10,
@@ -97,6 +98,13 @@ const mockScript2: RehydratingScript = {
9798
buildId: ''
9899
};
99100

101+
const mockResource: RehydratingResource = {
102+
url: 'example.com',
103+
frame: 'ABCDE',
104+
content: '<html>',
105+
mimeType: 'text/html',
106+
};
107+
100108
describe('RehydratingSession', () => {
101109
const sessionId = 1;
102110
const messageId = 1;
@@ -105,6 +113,7 @@ describe('RehydratingSession', () => {
105113
let mockRehydratingSession: SDK.RehydratingConnection.RehydratingSession;
106114
const executionContextsForTarget1 = [mockExecutionContext1, mockExecutionContext2];
107115
const scriptsForTarget1 = [mockScript1, mockScript2];
116+
const resourcesForTarget1 = [mockResource];
108117

109118
class MockRehydratingConnection implements SDK.RehydratingConnection.RehydratingConnectionInterface {
110119
messageQueue: ServerMessage[] = [];
@@ -126,7 +135,8 @@ describe('RehydratingSession', () => {
126135
beforeEach(() => {
127136
mockRehydratingConnection = new MockRehydratingConnection();
128137
mockRehydratingSession = new RehydratingSessionForTest(
129-
sessionId, target, executionContextsForTarget1, scriptsForTarget1, mockRehydratingConnection);
138+
sessionId, target, executionContextsForTarget1, scriptsForTarget1, resourcesForTarget1,
139+
mockRehydratingConnection);
130140
mockRehydratingSession.declareSessionAttachedToTarget();
131141
});
132142

@@ -142,25 +152,6 @@ describe('RehydratingSession', () => {
142152
target.targetId.toString());
143153
});
144154

145-
it('sends script parsed and debugger id while handling debugger enable', async function() {
146-
mockRehydratingConnection.clearMessageQueue();
147-
mockRehydratingSession.handleFrontendMessageAsFakeCDPAgent({
148-
id: messageId,
149-
method: 'Debugger.enable',
150-
sessionId,
151-
});
152-
assert.lengthOf(mockRehydratingConnection.messageQueue, 3);
153-
const scriptParsedMessages = mockRehydratingConnection.messageQueue.slice(0, 2);
154-
const resultMessage = mockRehydratingConnection.messageQueue.slice(2);
155-
for (const scriptParsedMessage of scriptParsedMessages) {
156-
assert.strictEqual(scriptParsedMessage.method, 'Debugger.scriptParsed');
157-
assert.strictEqual((scriptParsedMessage.params as RehydratingScript).isolate, target.isolate);
158-
}
159-
assert.isNotNull(resultMessage[0]);
160-
assert.strictEqual(resultMessage[0].id, messageId);
161-
assert.isNotNull((resultMessage[0].result as Protocol.Debugger.EnableResponse).debuggerId);
162-
});
163-
164155
it('sends execution context created while handling runtime enable', async function() {
165156
mockRehydratingConnection.clearMessageQueue();
166157
mockRehydratingSession.handleFrontendMessageAsFakeCDPAgent({

front_end/core/sdk/RehydratingConnection.ts

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import * as Root from '../root/root.js';
3232

3333
import * as EnhancedTraces from './EnhancedTracesParser.js';
3434
import type {
35-
ProtocolMessage, RehydratingExecutionContext, RehydratingScript, RehydratingTarget, ServerMessage} from
36-
'./RehydratingObject.js';
35+
ProtocolMessage, RehydratingExecutionContext, RehydratingResource, RehydratingScript, RehydratingTarget,
36+
ServerMessage} from './RehydratingObject.js';
3737
import {TraceObject} from './TraceObject.js';
3838

3939
const UIStrings = {
@@ -157,6 +157,7 @@ export class RehydratingConnection implements ProtocolClient.ConnectionTransport
157157
const target = hydratingDataPerTarget.target;
158158
const executionContexts = hydratingDataPerTarget.executionContexts;
159159
const scripts = hydratingDataPerTarget.scripts;
160+
const resources = hydratingDataPerTarget.resources;
160161
this.postToFrontend({
161162
method: 'Target.targetCreated',
162163
params: {
@@ -172,7 +173,7 @@ export class RehydratingConnection implements ProtocolClient.ConnectionTransport
172173
});
173174

174175
sessionId += 1;
175-
const session = new RehydratingSession(sessionId, target, executionContexts, scripts, this);
176+
const session = new RehydratingSession(sessionId, target, executionContexts, scripts, resources, this);
176177
this.sessions.set(sessionId, session);
177178
session.declareSessionAttachedToTarget();
178179
}
@@ -267,15 +268,17 @@ export class RehydratingSession extends RehydratingSessionBase {
267268
target: RehydratingTarget;
268269
executionContexts: RehydratingExecutionContext[] = [];
269270
scripts: RehydratingScript[] = [];
271+
resources: RehydratingResource[] = [];
270272

271273
constructor(
272274
sessionId: number, target: RehydratingTarget, executionContexts: RehydratingExecutionContext[],
273-
scripts: RehydratingScript[], connection: RehydratingConnectionInterface) {
275+
scripts: RehydratingScript[], resources: RehydratingResource[], connection: RehydratingConnectionInterface) {
274276
super(connection);
275277
this.sessionId = sessionId;
276278
this.target = target;
277279
this.executionContexts = executionContexts;
278280
this.scripts = scripts;
281+
this.resources = resources;
279282
}
280283

281284
override sendMessageToFrontend(payload: ServerMessage, attachSessionId = true): void {
@@ -294,12 +297,31 @@ export class RehydratingSession extends RehydratingSessionBase {
294297
case 'Debugger.enable':
295298
this.handleDebuggerEnable(data.id);
296299
break;
300+
case 'CSS.enable':
301+
this.sendMessageToFrontend({
302+
id: data.id,
303+
result: {},
304+
});
305+
break;
297306
case 'Debugger.getScriptSource':
298307
if (data.params) {
299308
const params = data.params as Protocol.Debugger.GetScriptSourceRequest;
300309
this.handleDebuggerGetScriptSource(data.id, params.scriptId);
301310
}
302311
break;
312+
case 'Page.getResourceTree':
313+
this.handleGetResourceTree(data.id);
314+
break;
315+
case 'Page.getResourceContent': {
316+
const request = data.params as unknown as Protocol.Page.GetResourceContentRequest;
317+
this.handleGetResourceContent(request.frameId, request.url, data.id);
318+
break;
319+
}
320+
case 'CSS.getStyleSheetText': {
321+
const request = data.params as unknown as Protocol.CSS.GetStyleSheetTextRequest;
322+
this.handleGetStyleSheetText(request.styleSheetId, data.id);
323+
break;
324+
}
303325
default:
304326
this.sendMessageToFrontend({
305327
id: data.id,
@@ -368,7 +390,22 @@ export class RehydratingSession extends RehydratingSessionBase {
368390
// script parsed event to communicate the current script state and respond with a mock
369391
// debugger id.
370392
private handleDebuggerEnable(id: number): void {
393+
const htmlResourceUrls = new Set(this.resources.filter(r => r.mimeType === 'text/html').map(r => r.url));
394+
371395
for (const script of this.scripts) {
396+
// Handle inline scripts.
397+
if (htmlResourceUrls.has(script.url)) {
398+
script.embedderName = script.url;
399+
// We don't have the actual embedded offset from this trace event. Non-zero
400+
// values are important though: that is what `Script.isInlineScript()`
401+
// checks. Otherwise these scripts would try to show individually within the
402+
// Sources panel.
403+
script.startColumn = 1;
404+
script.startLine = 1;
405+
script.endColumn = 1;
406+
script.endLine = 1;
407+
}
408+
372409
this.sendMessageToFrontend({
373410
method: 'Debugger.scriptParsed',
374411
params: script,
@@ -383,4 +420,75 @@ export class RehydratingSession extends RehydratingSessionBase {
383420
},
384421
});
385422
}
423+
424+
private handleGetResourceTree(id: number): void {
425+
const resources = this.resources.filter(r => r.mimeType === 'text/html' || r.mimeType === 'text/css');
426+
if (!resources.length) {
427+
return;
428+
}
429+
430+
const frameTree = {
431+
frame: {
432+
id: this.target.targetId,
433+
url: this.target.url,
434+
},
435+
childFrames: [],
436+
resources: resources.map(r => ({
437+
url: r.url,
438+
type: r.mimeType === 'text/html' ? 'Document' : 'Stylesheet',
439+
mimeType: r.mimeType,
440+
contentSize: r.content.length,
441+
})),
442+
};
443+
444+
this.sendMessageToFrontend({
445+
id,
446+
result: {
447+
frameTree,
448+
},
449+
});
450+
451+
const stylesheets = this.resources.filter(r => r.mimeType === 'text/css');
452+
for (const stylesheet of stylesheets) {
453+
this.sendMessageToFrontend({
454+
method: 'CSS.styleSheetAdded',
455+
params: {
456+
header: {
457+
styleSheetId: `sheet.${stylesheet.frame}.${stylesheet.url}`,
458+
frameId: stylesheet.frame,
459+
sourceURL: stylesheet.url,
460+
},
461+
},
462+
});
463+
}
464+
}
465+
466+
private handleGetResourceContent(frame: string, url: string, id: number): void {
467+
const resource = this.resources.find(r => r.frame === frame && r.url === url);
468+
if (!resource) {
469+
return;
470+
}
471+
472+
this.sendMessageToFrontend({
473+
id,
474+
result: {
475+
content: resource.content,
476+
base64Encoded: false,
477+
},
478+
});
479+
}
480+
481+
private handleGetStyleSheetText(stylesheetId: string, id: number): void {
482+
const resource = this.resources.find(r => `sheet.${r.frame}.${r.url}` === stylesheetId);
483+
if (!resource) {
484+
return;
485+
}
486+
487+
this.sendMessageToFrontend({
488+
id,
489+
result: {
490+
text: resource.content,
491+
},
492+
});
493+
}
386494
}

front_end/core/sdk/RehydratingObject.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export interface RehydratingScript extends Protocol.Debugger.ScriptParsedEvent {
1414
pid: number;
1515
}
1616

17+
export interface RehydratingResource {
18+
url: string;
19+
content: string;
20+
frame: string;
21+
mimeType: string;
22+
}
23+
1724
export interface RehydratingExecutionContextAuxData {
1825
frameId?: Protocol.Page.FrameId;
1926
isDefault?: boolean;
@@ -39,6 +46,7 @@ export interface HydratingDataPerTarget {
3946
target: RehydratingTarget;
4047
executionContexts: RehydratingExecutionContext[];
4148
scripts: RehydratingScript[];
49+
resources: RehydratingResource[];
4250
}
4351

4452
export interface ProtocolMessage {

front_end/core/sdk/TraceObject.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import type * as Platform from '../../core/platform/platform.js';
77
import type * as Protocol from '../../generated/protocol.js';
88

99
import type {NetworkRequest} from './NetworkRequest.js';
10+
import type {RehydratingResource} from './RehydratingObject.js';
1011
import {ResourceTreeModel} from './ResourceTreeModel.js';
1112
import type {SourceMapV3} from './SourceMap.js';
1213

1314
/** A thin wrapper class, mostly to enable instanceof-based revealing of traces to open in Timeline. **/
1415
export class TraceObject {
1516
readonly traceEvents: Protocol.Tracing.DataCollectedEvent['value'];
16-
readonly metadata: {sourceMaps?: Array<{sourceMapUrl: string, sourceMap: SourceMapV3, url: string}>};
17+
readonly metadata: {
18+
sourceMaps?: Array<{sourceMapUrl: string, sourceMap: SourceMapV3, url: string}>,
19+
resources?: RehydratingResource[],
20+
};
1721
constructor(payload: Protocol.Tracing.DataCollectedEvent['value']|TraceObject, meta?: Object) {
1822
if (Array.isArray(payload)) {
1923
this.traceEvents = payload;

0 commit comments

Comments
 (0)