diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index 3d88bf1b8e..146ea0a602 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -47,7 +47,7 @@ FileIO marker schemas now have a `tableLabel`. ### Version 72 -Marker schema fields can now include a `containsPII` array describing the categories of privacy-sensitive data they contain. Profile sanitization uses these categories instead of identifying privacy-sensitive fields from the marker type. +Marker schema fields can now include a `containsPII` array describing the categories of privacy-sensitive data they contain. Profile sanitization uses these categories instead of identifying privacy-sensitive fields from the marker type. Extension-related text markers are converted to structured payloads so their extension IDs can be annotated separately. ### Version 71 diff --git a/src/profile-logic/marker-data.ts b/src/profile-logic/marker-data.ts index 6b86927cc6..226186c444 100644 --- a/src/profile-logic/marker-data.ts +++ b/src/profile-logic/marker-data.ts @@ -1458,18 +1458,6 @@ export function groupScreenshotsById( return idToScreenshotMarkers; } -function _removeExtensionId(markerName: string, text: string): string { - if (['ExtensionParent', 'ExtensionChild'].includes(markerName)) { - return text.replace(/^.*, (api_(call|event): )/, '$1'); - } - - if (markerName === 'Extension Suspend') { - return text.replace(/ by .*$/, ''); - } - - return text; -} - function _shouldSanitizePIICategory( category: MarkerSchemaPIICategory, PIIToBeRemoved: RemoveProfileInformation @@ -1518,7 +1506,6 @@ function _updateMarkerPayloadField( /** Apply a marker schema's PII rules to its payload. */ export function sanitizeMarkerFromSchema( markerSchema: MarkerSchema, - markerName: string, markerPayload: MarkerPayload, stringTable: StringTable, PIIToBeRemoved: RemoveProfileInformation @@ -1575,13 +1562,8 @@ export function sanitizeMarkerFromSchema( break; case 'extension-id': if (hasField) { - markerPayload = _updateMarkerPayloadField( - markerPayload, - key, - isStringIndex, - stringTable, - (text) => _removeExtensionId(markerName, text) - ); + markerPayload = { ...markerPayload }; + delete (markerPayload as any)[key]; } break; case 'preference-value': diff --git a/src/profile-logic/marker-schema.ts b/src/profile-logic/marker-schema.ts index 392c3e2868..dc8f8d500b 100644 --- a/src/profile-logic/marker-schema.ts +++ b/src/profile-logic/marker-schema.ts @@ -43,7 +43,7 @@ const markerSchemaPIICategoriesBySchemaName = new Map< ['isPrivateBrowsing', ['private-browsing']], ]), ], - ['Text', new Map([['name', ['url', 'extension-id']]])], + ['Text', new Map([['name', ['url']]])], ['PreferenceRead', new Map([['prefValue', ['preference-value']]])], ]); @@ -79,6 +79,29 @@ export function addPIICategoriesToMarkerSchemas( return markerSchemas.map(addPIICategoriesToMarkerSchema); } +export const extensionMarkerSchema: MarkerSchema = { + name: 'Extension', + tableLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + chartLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + display: ['marker-chart', 'marker-table'], + fields: [ + { + key: 'extensionId', + label: 'Extension ID', + format: 'string', + containsPII: ['extension-id'], + }, + { + key: 'name', + label: 'Details', + format: 'string', + containsPII: ['url'], + }, + ], +}; + /** * The marker schema comes from Gecko, and is embedded in the profile. However, * we may want to define schemas that are front-end only. This is the location diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index b3ad6342d3..c08fdc3a89 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -64,6 +64,7 @@ import { addPIICategoriesToMarkerSchema, addFileIoTableLabel, computeStringIndexMarkerFieldsByDataType, + extensionMarkerSchema, } from '../profile-logic/marker-schema'; import { convertJsTracerToThread } from '../profile-logic/js-tracer'; @@ -105,8 +106,10 @@ import type { IndexIntoGeckoThreadStringTable, GCSliceMarkerPayload, GCMajorMarkerPayload, + ExtensionMarkerPayload, MarkerPayload, MarkerPayload_Gecko, + TextMarkerPayload, GCSliceData_Gecko, GCMajorCompleted, GCMajorCompleted_Gecko, @@ -791,16 +794,16 @@ function _processMarkers( } } + const markerName = stringArray[geckoMarkers.name[markerIndex]]; const payload = _processMarkerPayload( + markerName, geckoPayload, stringArray, stringTable, stringIndexMarkerFieldsByDataType, stackIndexOffset ); - const name = stringTable.indexForString( - stringArray[geckoMarkers.name[markerIndex]] - ); + const name = stringTable.indexForString(markerName); const startTime = geckoMarkers.startTime[markerIndex]; const endTime = geckoMarkers.endTime[markerIndex]; const phase = geckoMarkers.phase[markerIndex]; @@ -854,11 +857,59 @@ function convertPhaseTimes( return phases; } +function _getExtensionMarkerPayloadFields( + markerName: string, + text: string +): Pick | null { + switch (markerName) { + case 'ExtensionParent': + case 'ExtensionChild': { + const match = /^(.*), (api_(?:call|event): [\s\S]*)$/.exec(text); + return match + ? { + type: 'Extension', + name: match[2], + extensionId: match[1], + } + : null; + } + case 'Extension Suspend': { + const match = / by .*$/.exec(text); + return match === null + ? null + : { + type: 'Extension', + name: text.slice(0, match.index), + extensionId: text.slice(match.index + ' by '.length), + }; + } + default: + return null; + } +} + +function _processExtensionTextMarkerPayload( + markerName: string, + payload: TextMarkerPayload, + stringArray: string[] +): ExtensionMarkerPayload | null { + const text = + typeof payload.name === 'number' ? stringArray[payload.name] : payload.name; + const fields = _getExtensionMarkerPayloadFields(markerName, text); + if (!fields) { + return null; + } + + const { type: _type, name: _name, ...otherFields } = payload; + return { ...otherFields, ...fields }; +} + /** - * Process just the marker payload. This converts stacks into causes, and augments - * the GC information. + * Process just the marker payload. This converts stacks into causes, augments + * the GC information, and converts extension text markers into structured payloads. */ function _processMarkerPayload( + markerName: string, geckoPayload: MarkerPayload_Gecko | null, stringArray: string[], stringTable: StringTable, @@ -875,6 +926,17 @@ function _processMarkerPayload( // Warning: This function converts the payload into an any type. const payload = _convertStackToCause(geckoPayload, stackIndexOffset); + if (payload.type === 'Text') { + const extensionPayload = _processExtensionTextMarkerPayload( + markerName, + payload, + stringArray + ); + if (extensionPayload) { + return extensionPayload; + } + } + switch (payload.type) { /* * We want to improve the format of these markers to make them @@ -1765,6 +1827,32 @@ function processMarkerSchema(geckoProfile: GeckoProfile): MarkerSchema[] { } } + let isExtensionMarkerSchemaUsed = false; + for (const profile of [geckoProfile, ...geckoProfile.processes]) { + for (const thread of profile.threads) { + const { markers, stringTable } = thread; + for (const marker of markers.data) { + const payload = marker[markers.schema.data]; + if (!payload || payload.type !== 'Text') { + continue; + } + const markerName = stringTable[marker[markers.schema.name]]; + const text = + typeof payload.name === 'number' + ? stringTable[payload.name] + : payload.name; + const fields = _getExtensionMarkerPayloadFields(markerName, text); + if (fields) { + isExtensionMarkerSchemaUsed = true; + } + } + } + } + + if (isExtensionMarkerSchemaUsed && !names.has(extensionMarkerSchema.name)) { + combinedSchemas.push(extensionMarkerSchema); + } + return combinedSchemas; } diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 8fb6eda4e0..794048ac0b 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3434,6 +3434,87 @@ const _upgraders: { } }, [72]: (profile: any) => { + const extensionMarkerSchema = { + name: 'Extension', + tableLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + chartLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + display: ['marker-chart', 'marker-table'], + fields: [ + { + key: 'extensionId', + label: 'Extension ID', + format: 'string', + containsPII: ['extension-id'], + }, + { + key: 'name', + label: 'Details', + format: 'string', + containsPII: ['url'], + }, + ], + }; + + let isExtensionMarkerSchemaUsed = false; + const stringArray = profile.shared.stringArray; + for (const thread of profile.threads) { + const { markers } = thread; + for (let markerIndex = 0; markerIndex < markers.length; markerIndex++) { + const payload = markers.data[markerIndex]; + if (!payload || payload.type !== 'Text') { + continue; + } + + const markerName = stringArray[markers.name[markerIndex]]; + const text = + typeof payload.name === 'number' + ? stringArray[payload.name] + : payload.name; + if (typeof text !== 'string') { + continue; + } + + if ( + markerName === 'ExtensionParent' || + markerName === 'ExtensionChild' + ) { + const match = /^(.*), (api_(?:call|event): [\s\S]*)$/.exec(text); + if (match) { + markers.data[markerIndex] = { + ...payload, + type: 'Extension', + name: match[2], + extensionId: match[1], + }; + isExtensionMarkerSchemaUsed = true; + } + } else if (markerName === 'Extension Suspend') { + const match = / by .*$/.exec(text); + if (match) { + markers.data[markerIndex] = { + ...payload, + type: 'Extension', + name: text.slice(0, match.index), + extensionId: text.slice(match.index + ' by '.length), + }; + isExtensionMarkerSchemaUsed = true; + } + } + } + } + + const schemaNames = new Set( + profile.meta.markerSchema.map((schema: any) => schema.name) + ); + if ( + isExtensionMarkerSchemaUsed && + !schemaNames.has(extensionMarkerSchema.name) + ) { + profile.meta.markerSchema.push(extensionMarkerSchema); + } + const piiCategoriesBySchemaName = new Map>([ [ 'Network', @@ -3443,7 +3524,7 @@ const _upgraders: { ['isPrivateBrowsing', ['private-browsing']], ]), ], - ['Text', new Map([['name', ['url', 'extension-id']]])], + ['Text', new Map([['name', ['url']]])], ['PreferenceRead', new Map([['prefValue', ['preference-value']]])], ]); diff --git a/src/profile-logic/sanitize.ts b/src/profile-logic/sanitize.ts index 5b0adeb2bc..614fadfa19 100644 --- a/src/profile-logic/sanitize.ts +++ b/src/profile-logic/sanitize.ts @@ -499,10 +499,8 @@ function sanitizeThreadPII( currentMarker ); if (markerSchema) { - const markerName = stringTable.getString(markerTable.name[i]); const sanitizedMarker = sanitizeMarkerFromSchema( markerSchema, - markerName, currentMarker, stringTable, PIIToBeRemoved diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index f020e95137..d39678551b 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -261,7 +261,6 @@ Object { Object { "containsPII": Array [ "url", - "extension-id", ], "format": "string", "key": "name", diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index a4865443e4..6d77cbb21c 100644 --- a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap @@ -7596,7 +7596,6 @@ Object { Object { "containsPII": Array [ "url", - "extension-id", ], "format": "string", "key": "name", @@ -9015,7 +9014,6 @@ Object { Object { "containsPII": Array [ "url", - "extension-id", ], "format": "string", "key": "name", @@ -10602,7 +10600,6 @@ Object { Object { "containsPII": Array [ "url", - "extension-id", ], "format": "string", "key": "name", diff --git a/src/test/unit/marker-schema.test.ts b/src/test/unit/marker-schema.test.ts index 59e08fbfa5..9ed992dbdd 100644 --- a/src/test/unit/marker-schema.test.ts +++ b/src/test/unit/marker-schema.test.ts @@ -6,6 +6,7 @@ import { FILE_IO_TABLE_LABEL, formatFromMarkerSchema, parseLabel, + extensionMarkerSchema, markerSchemaFrontEndOnly, } from '../../profile-logic/marker-schema'; import { renderMarkerFieldValue } from 'firefox-profiler/components/tooltip/Marker'; @@ -77,6 +78,31 @@ describe('marker schema labels', function () { expect(console.error).toHaveBeenCalledTimes(0); }); + it('formats extension marker labels from structured fields', function () { + for (const label of [ + extensionMarkerSchema.tableLabel, + extensionMarkerSchema.chartLabel, + ]) { + expect( + applyLabel({ + schemaFields: extensionMarkerSchema.fields, + label: label as string, + payload: { + extensionId: 'addon@example.com', + name: 'api_call: tabs.query', + }, + }) + ).toBe('addon@example.com, api_call: tabs.query'); + expect( + applyLabel({ + schemaFields: extensionMarkerSchema.fields, + label: label as string, + payload: { name: 'api_call: tabs.query' }, + }) + ).toBe('api_call: tabs.query'); + } + }); + it('can parse a label with just a lookup value', function () { expect( applyLabel({ diff --git a/src/test/unit/process-profile.test.ts b/src/test/unit/process-profile.test.ts index 71014b9934..384080b7a2 100644 --- a/src/test/unit/process-profile.test.ts +++ b/src/test/unit/process-profile.test.ts @@ -15,6 +15,7 @@ import { computeTimeColumnForRawSamplesTable } from '../../profile-logic/profile import { StringTable } from '../../utils/string-table'; import { createGeckoProfile, + createGeckoProfileWithMarkers, createGeckoCounter, createGeckoMarkerStack, createGeckoProfilerOverhead, @@ -1129,7 +1130,7 @@ describe('Marker schema conversion', function () { { key: 'name', format: 'unique-string', - containsPII: ['url', 'extension-id'], + containsPII: ['url'], }, ]); expect(schemasByName.PreferenceRead.fields).toEqual([ @@ -1167,6 +1168,83 @@ describe('Marker schema conversion', function () { expect(getConvertedFileIoTableLabel(geckoTableLabel)).toBe(geckoTableLabel); }); + it('should convert extension text markers to structured payloads', function () { + const extensionChildText = 'child@example.com, api_call: tabs.query'; + const geckoProfile = createGeckoProfileWithMarkers([ + { + name: 'ExtensionParent', + startTime: 0, + endTime: 1, + phase: 1, + data: { + type: 'Text', + name: 'parent@example.com, api_event: runtime.onMessage', + }, + }, + { + name: 'ExtensionChild', + startTime: 1, + endTime: 2, + phase: 1, + data: { type: 'Text', name: extensionChildText }, + }, + { + name: 'Extension Suspend', + startTime: 2, + endTime: 3, + phase: 1, + data: { + type: 'Text', + name: 'onBeforeRequest https://example.com by addon@example.com (chanId: 42)', + }, + }, + ]); + const geckoThread = geckoProfile.threads[0]; + const extensionChildPayload = geckoThread.markers.data[1][5]; + if (!extensionChildPayload || extensionChildPayload.type !== 'Text') { + throw new Error('Expected a Text marker'); + } + extensionChildPayload.name = + geckoThread.stringTable.push(extensionChildText) - 1; + + const processedProfile = processGeckoProfile(geckoProfile); + + expect(processedProfile.threads[0].markers.data).toEqual([ + { + type: 'Extension', + name: 'api_event: runtime.onMessage', + extensionId: 'parent@example.com', + }, + { + type: 'Extension', + name: 'api_call: tabs.query', + extensionId: 'child@example.com', + }, + { + type: 'Extension', + name: 'onBeforeRequest https://example.com', + extensionId: 'addon@example.com (chanId: 42)', + }, + ]); + const schemasByName = Object.fromEntries( + processedProfile.meta.markerSchema.map((schema) => [schema.name, schema]) + ); + expect(schemasByName.Extension.fields).toEqual([ + { + key: 'extensionId', + label: 'Extension ID', + format: 'string', + containsPII: ['extension-id'], + }, + { + key: 'name', + label: 'Details', + format: 'string', + containsPII: ['url'], + }, + ]); + }); + it('should preserve optional marker schema properties', function () { const geckoProfile = createGeckoProfile(); diff --git a/src/test/unit/profile-upgrading.test.ts b/src/test/unit/profile-upgrading.test.ts index 50a7d6ceaa..2184bb666a 100644 --- a/src/test/unit/profile-upgrading.test.ts +++ b/src/test/unit/profile-upgrading.test.ts @@ -7,10 +7,12 @@ import { serializeProfileToJsonString, } from '../../profile-logic/process-profile'; import { upgradeGeckoProfileToCurrentVersion } from '../../profile-logic/gecko-profile-versioning'; +import { attemptToUpgradeProcessedProfileThroughMutation } from '../../profile-logic/processed-profile-versioning'; import { GECKO_PROFILE_VERSION, PROCESSED_PROFILE_VERSION, } from '../../app-logic/constants'; +import { getProfileWithMarkers } from '../fixtures/profiles/processed-profile'; /* eslint-disable jest/expect-expect */ // testProfileUpgrading is an assertion, although eslint doesn't realize it. Disable @@ -134,6 +136,142 @@ describe('upgrading processed profiles', function () { require('../fixtures/upgrades/processed-3.json') ); }); + + it('adds PII categories and structures extension markers', function () { + const profile = getProfileWithMarkers([ + [ + 'ExtensionParent', + 0, + 1, + { + type: 'Text', + name: 'parent@example.com, api_call: tabs.query', + }, + ], + [ + 'ExtensionChild', + 1, + 2, + { + type: 'Text', + name: 'child@example.com, api_event: runtime.onMessage', + }, + ], + [ + 'Extension Suspend', + 2, + 3, + { + type: 'Text', + name: 'onBeforeRequest https://example.com by addon@example.com (chanId: 42)', + }, + ], + ]); + profile.meta.preprocessedProfileVersion = 71; + profile.meta.markerSchema = [ + { name: 'Network', display: [], fields: [] }, + { + name: 'Text', + display: [], + fields: [{ key: 'name', format: 'unique-string' }], + }, + { + name: 'PreferenceRead', + display: [], + fields: [{ key: 'prefValue', format: 'string' }], + }, + ]; + + attemptToUpgradeProcessedProfileThroughMutation(profile, {}); + + expect(profile.meta.markerSchema).toEqual([ + { + name: 'Network', + display: [], + fields: [ + { + key: 'URI', + format: 'string', + hidden: true, + containsPII: ['url'], + }, + { + key: 'RedirectURI', + format: 'string', + hidden: true, + containsPII: ['url'], + }, + { + key: 'isPrivateBrowsing', + format: 'string', + hidden: true, + containsPII: ['private-browsing'], + }, + ], + }, + { + name: 'Text', + display: [], + fields: [ + { + key: 'name', + format: 'unique-string', + containsPII: ['url'], + }, + ], + }, + { + name: 'PreferenceRead', + display: [], + fields: [ + { + key: 'prefValue', + format: 'string', + containsPII: ['preference-value'], + }, + ], + }, + { + name: 'Extension', + tableLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + chartLabel: + "{marker.data.extensionId}{marker.data.extensionId ? ', ' : ''}{marker.data.name}", + display: ['marker-chart', 'marker-table'], + fields: [ + { + key: 'extensionId', + label: 'Extension ID', + format: 'string', + containsPII: ['extension-id'], + }, + { + key: 'name', + label: 'Details', + format: 'string', + containsPII: ['url'], + }, + ], + }, + ]); + expect(profile.threads[0].markers.data).toEqual([ + { + type: 'Extension', + name: 'api_call: tabs.query', + extensionId: 'parent@example.com', + }, + { + type: 'Extension', + name: 'api_event: runtime.onMessage', + extensionId: 'child@example.com', + }, + { + type: 'Extension', + name: 'onBeforeRequest https://example.com', + extensionId: 'addon@example.com (chanId: 42)', + }, + ]); + }); }); describe('importing perf profile', function () { diff --git a/src/test/unit/sanitize.test.ts b/src/test/unit/sanitize.test.ts index eb47aab689..eefa1b0615 100644 --- a/src/test/unit/sanitize.test.ts +++ b/src/test/unit/sanitize.test.ts @@ -21,7 +21,10 @@ import { correlateIPCMarkers, deriveMarkersFromRawMarkerTable, } from '../../profile-logic/marker-data'; -import { addPIICategoriesToMarkerSchema } from '../../profile-logic/marker-schema'; +import { + addPIICategoriesToMarkerSchema, + extensionMarkerSchema, +} from '../../profile-logic/marker-schema'; import { getTimeRangeForThread, computeTimeColumnForRawSamplesTable, @@ -201,6 +204,10 @@ describe('sanitizePII', function () { }), }; + const extensionMarkerSchemaByName: MarkerSchemaByName = { + Extension: extensionMarkerSchema, + }; + function setupWithUniqueStringTextMarkers( piiConfig: Partial, markers: Array<[string, string]> @@ -248,76 +255,66 @@ describe('sanitizePII', function () { expect( setupWithUniqueStringTextMarkers({ shouldRemoveUrls: true }, [ [ - 'Extension Suspend', + 'Text marker', 'onBeforeRequest https://profiler.firefox.com/ by extension', ], ]) ).toEqual(['onBeforeRequest https:// by extension']); }); - it('should sanitize extension ids inside text markers holding a unique string', function () { - expect( - setupWithUniqueStringTextMarkers({ shouldRemoveExtensions: true }, [ - [ - 'ExtensionParent', - 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener', - ], - [ - 'ExtensionChild', - 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener', - ], - ]) - ).toEqual([ - 'api_call: runtime.onUpdateAvailable.addListener', - 'api_call: runtime.onUpdateAvailable.addListener', - ]); - }); - - it('should sanitize both URLs and extension ids inside text markers holding a unique string', function () { - expect( - setupWithUniqueStringTextMarkers( - { shouldRemoveUrls: true, shouldRemoveExtensions: true }, - [ - [ - 'Extension Suspend', - 'onBeforeRequest https://profiler.firefox.com/ by extension', - ], - ] - ) - ).toEqual(['onBeforeRequest https://']); - }); - - it('should not alter other strings when sanitizing a shared text marker string', function () { - // The string table is shared, so the sanitized text has to be interned as a - // new string instead of replacing an entry others may point to. - const text = - 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener'; + it('should not alter other references to a sanitized unique string', function () { + const extensionId = 'formautofill@mozilla.org'; const profile = getProfileWithMarkers([ - ['ExtensionParent', 0, 1, { type: 'Text', name: text }], - ['SomeOtherMarker', 0, 1, { type: 'Text', name: text }], + [ + 'ExtensionParent', + 0, + 1, + { + type: 'Extension', + name: 'api_call: runtime.onUpdateAvailable.addListener', + extensionId, + }, + ], + ['SomeOtherMarker', 0, 1, { type: 'Text', name: extensionId }], ]); - profile.meta.markerSchema = [uniqueStringTextSchema.Text]; + const markerSchemaByName: MarkerSchemaByName = { + Extension: { + name: 'Extension', + display: ['marker-chart', 'marker-table'], + fields: [ + { + key: 'extensionId', + format: 'unique-string', + containsPII: ['extension-id'], + }, + ], + }, + Text: uniqueStringTextSchema.Text, + }; + profile.meta.markerSchema = Object.values(markerSchemaByName); const stringTable = StringTable.withBackingArray( profile.shared.stringArray ); - const textIndex = stringTable.indexForString(text); + const extensionIdIndex = stringTable.indexForString(extensionId); profile.threads[0].markers.data = [ - { type: 'Text', name: textIndex }, - { type: 'Text', name: textIndex }, + { + type: 'Extension', + name: 'api_call: runtime.onUpdateAvailable.addListener', + extensionId: extensionIdIndex, + } as any, + { type: 'Text', name: extensionIdIndex }, ]; const { sanitizedProfile } = setup( { shouldRemoveExtensions: true }, profile, - uniqueStringTextSchema + markerSchemaByName ); const { stringArray } = sanitizedProfile.shared; - const [sanitized, untouched] = sanitizedProfile.threads[0].markers.data.map( - (data) => stringArray[(data as any).name] - ); - expect(sanitized).toBe('api_call: runtime.onUpdateAvailable.addListener'); - expect(untouched).toBe(text); + const [sanitized, untouched] = sanitizedProfile.threads[0].markers.data; + expect((sanitized as any).extensionId).toBeUndefined(); + expect(stringArray[(untouched as any).name]).toBe(extensionId); }); it('should sanitize the threads if they are provided', function () { @@ -680,10 +677,9 @@ describe('sanitizePII', function () { ).toEqual(['Load 0: https://', 'Load 0: https://']); }); - it('should sanitize URLs inside text markers after upgrading an old processed profile', function () { + it('should sanitize URLs after upgrading an extension text marker', function () { const unsanitizedNameField = 'onBeforeRequest https://profiler.firefox.com/ by extension'; - const sanitizedNameField = 'onBeforeRequest https:// by extension'; const originalProfile = getProfileWithMarkers([ [ 'Extension Suspend', @@ -704,10 +700,14 @@ describe('sanitizePII', function () { ); const marker = sanitizedProfile.threads[0].markers.data[0]; - if (!marker || marker.type !== 'Text') { - throw new Error('Expected a Text marker'); + if (!marker || marker.type !== 'Extension') { + throw new Error('Expected an Extension marker'); } - expect(marker).toEqual({ type: 'Text', name: sanitizedNameField }); + expect(marker).toEqual({ + type: 'Extension', + name: 'onBeforeRequest https://', + extensionId: 'extension', + }); }); it('should sanitize all the URLs inside string table', function () { @@ -744,14 +744,11 @@ describe('sanitizePII', function () { } }); - it('should sanitize extension ids inside text markers', function () { - const unsanitizedNameField = - 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener'; - const sanitizedNameField = - 'api_call: runtime.onUpdateAvailable.addListener'; + it('should sanitize PII inside structured extension markers', function () { const { sanitizedProfile } = setup( { shouldRemoveExtensions: true, + shouldRemoveUrls: true, }, getProfileWithMarkers([ [ @@ -759,8 +756,9 @@ describe('sanitizePII', function () { 0, 1, { - type: 'Text', - name: unsanitizedNameField, + type: 'Extension', + name: 'api_call: tabs.open https://example.com/', + extensionId: 'formautofill@mozilla.org', }, ], [ @@ -768,26 +766,28 @@ describe('sanitizePII', function () { 0, 1, { - type: 'Text', - name: unsanitizedNameField, + type: 'Extension', + name: 'api_call: tabs.open https://example.com/', + extensionId: 'formautofill@mozilla.org', }, ], - ]) + ]), + extensionMarkerSchemaByName ); const markers = sanitizedProfile.threads[0].markers; for (const marker of [markers.data[0], markers.data[1]]) { - if (!marker || marker.type !== 'Text') { - throw new Error('Expected a Text marker'); + if (!marker || marker.type !== 'Extension') { + throw new Error('Expected an extension marker'); } - expect(marker).toEqual({ type: 'Text', name: sanitizedNameField }); + expect(marker).toEqual({ + type: 'Extension', + name: 'api_call: tabs.open https://', + }); } }); it('should sanitize both URLs and extension ids inside Extension Suspend markers', function () { - const unsanitizedNameField = - 'onBeforeRequest https://profiler.firefox.com/ by extension'; - const sanitizedNameField = 'onBeforeRequest https://'; const { sanitizedProfile } = setup( { shouldRemoveUrls: true, @@ -799,18 +799,23 @@ describe('sanitizePII', function () { 0, 1, { - type: 'Text', - name: unsanitizedNameField, + type: 'Extension', + name: 'onBeforeRequest https://profiler.firefox.com/', + extensionId: 'extension', }, ], - ]) + ]), + extensionMarkerSchemaByName ); const marker = sanitizedProfile.threads[0].markers.data[0]; - if (!marker || marker.type !== 'Text') { - throw new Error('Expected a Text marker'); + if (!marker || marker.type !== 'Extension') { + throw new Error('Expected an Extension marker'); } - expect(marker).toEqual({ type: 'Text', name: sanitizedNameField }); + expect(marker).toEqual({ + type: 'Extension', + name: 'onBeforeRequest https://', + }); }); it('should not sanitize all the preference values inside preference read markers', function () { diff --git a/src/types/markers.ts b/src/types/markers.ts index 9f71c92517..11fea30670 100644 --- a/src/types/markers.ts +++ b/src/types/markers.ts @@ -647,6 +647,14 @@ export type TextMarkerPayload = { innerWindowID?: number; }; +export type ExtensionMarkerPayload = { + type: 'Extension'; + name: string; + extensionId?: string; + cause?: CauseBacktrace; + innerWindowID?: number; +}; + // Any import from a Chrome profile export type ChromeEventPayload = { type: string; @@ -872,6 +880,7 @@ export type MarkerPayload = | NetworkPayload | UserTimingMarkerPayload | TextMarkerPayload + | ExtensionMarkerPayload | LogMarkerPayload | PaintProfilerMarkerTracing | CcMarkerTracing