From 8a50e90592a195496141906ee0ed9a0aa5364c24 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:47:52 -0500 Subject: [PATCH] wip --- .../src/helpers/base-page-object.ts | 44 +++++------ .../src/helpers/network-utils.ts | 25 ++++--- .../src/helpers/playwright-utils.ts | 51 +++++++++++++ .../src/helpers/ts.ts | 1 + .../all-segment-events.test.ts | 8 +- .../src/tests/signals-vanilla/basic.test.ts | 3 +- .../button-click-complex.test.ts | 66 ++++++++++------ .../signals-vanilla/change-input.test.ts | 52 +++++++++++++ .../src/tests/signals-vanilla/index-page.ts | 9 +++ .../network-signals-filter.test.ts | 9 +-- .../network-signals-xhr.test.ts | 11 +-- .../signals-vanilla/signals-redaction.test.ts | 63 ++++++++++++++++ .../src/core/client/__tests__/client.test.ts | 31 +++++++- .../src/core/client/__tests__/redact.test.ts | 75 ++++++++++++++++++- .../signals/signals/src/core/client/index.ts | 10 +-- .../signals/signals/src/core/client/redact.ts | 18 ++++- .../__tests__/signals-runtime.test.ts | 2 +- packages/signals/signals/src/types/signals.ts | 2 +- 18 files changed, 387 insertions(+), 93 deletions(-) create mode 100644 packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts create mode 100644 packages/signals/signals-integration-tests/src/helpers/ts.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts create mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts diff --git a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts index 0ce504ca9..61484cefb 100644 --- a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts +++ b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts @@ -1,15 +1,17 @@ import { CDNSettingsBuilder } from '@internal/test-helpers' -import { Page, Request } from '@playwright/test' +import { Page } from '@playwright/test' import { logConsole } from './log-console' -import { SegmentEvent } from '@segment/analytics-next' import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals' -import { PageNetworkUtils, SignalAPIRequestBuffer } from './network-utils' +import { + PageNetworkUtils, + SignalAPIRequestBuffer, + TrackingAPIRequestBuffer, +} from './network-utils' export class BasePage { protected page!: Page public signalsAPI = new SignalAPIRequestBuffer() - public lastTrackingApiReq!: Request - public trackingApiReqs: SegmentEvent[] = [] + public trackingAPI = new TrackingAPIRequestBuffer() public url: string public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js' public edgeFn!: string @@ -28,9 +30,7 @@ export class BasePage { * and wait for analytics and signals to be initialized */ async loadAndWait(...args: Parameters) { - await this.load(...args) - await this.waitForSignalsAssets() - return this + await Promise.all([this.load(...args), this.waitForSettings()]) } /** @@ -39,22 +39,24 @@ export class BasePage { async load( page: Page, edgeFn: string, - signalSettings: Partial = {} + signalSettings: Partial = {}, + options: { updateURL?: (url: string) => string } = {} ) { logConsole(page) this.page = page this.network = new PageNetworkUtils(page) this.edgeFn = edgeFn await this.setupMockedRoutes() - await this.page.goto(this.url) + const url = options.updateURL ? options.updateURL(this.url) : this.url + await this.page.goto(url) await this.invokeAnalyticsLoad(signalSettings) } /** * Wait for analytics and signals to be initialized + * We could do the same thing with analytics.ready() and signalsPlugin.ready() */ - async waitForSignalsAssets() { - // this is kind of an approximation of full initialization + async waitForSettings() { return Promise.all([ this.waitForCDNSettingsResponse(), this.waitForEdgeFunctionResponse(), @@ -72,7 +74,6 @@ export class BasePage { ({ signalSettings }) => { window.signalsPlugin = new window.SignalsPlugin({ disableSignalsRedaction: true, - flushInterval: 1000, ...signalSettings, }) window.analytics.load({ @@ -87,7 +88,7 @@ export class BasePage { private async setupMockedRoutes() { // clear any existing saved requests - this.trackingApiReqs = [] + this.trackingAPI.clear() this.signalsAPI.clear() await Promise.all([ @@ -99,11 +100,10 @@ export class BasePage { async mockTrackingApi() { await this.page.route('https://api.segment.io/v1/*', (route, request) => { - this.lastTrackingApiReq = request - this.trackingApiReqs.push(request.postDataJSON()) if (request.method().toLowerCase() !== 'post') { throw new Error(`Unexpected method: ${request.method()}`) } + this.trackingAPI.addRequest(request) return route.fulfill({ contentType: 'application/json', status: 201, @@ -122,10 +122,10 @@ export class BasePage { await this.page.route( 'https://signals.segment.io/v1/*', (route, request) => { - this.signalsAPI.addRequest(request) if (request.method().toLowerCase() !== 'post') { throw new Error(`Unexpected method: ${request.method()}`) } + this.signalsAPI.addRequest(request) return route.fulfill({ contentType: 'application/json', status: 201, @@ -241,15 +241,17 @@ export class BasePage { }) } - waitForEdgeFunctionResponse() { + waitForEdgeFunctionResponse(timeout = 30000) { return this.page.waitForResponse( - `https://cdn.edgefn.segment.com/MY-WRITEKEY/**` + `https://cdn.edgefn.segment.com/MY-WRITEKEY/**`, + { timeout } ) } - waitForCDNSettingsResponse() { + async waitForCDNSettingsResponse(timeout = 30000) { return this.page.waitForResponse( - 'https://cdn.segment.com/v1/projects/*/settings' + 'https://cdn.segment.com/v1/projects/*/settings', + { timeout } ) } diff --git a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts b/packages/signals/signals-integration-tests/src/helpers/network-utils.ts index fb1f20bc3..e2784c811 100644 --- a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts +++ b/packages/signals/signals-integration-tests/src/helpers/network-utils.ts @@ -1,5 +1,6 @@ import { Page, Route, Request } from '@playwright/test' import { SegmentEvent } from '@segment/analytics-next' +import { Signal } from '@segment/analytics-signals' type FulfillOptions = Parameters['0'] export interface XHRRequestOptions { @@ -70,8 +71,6 @@ export class PageNetworkUtils { body: JSON.stringify({ foo: 'bar' }), ...args.request, }) - .then(console.log) - .catch(console.error) }, { url, request } ) @@ -113,18 +112,23 @@ export class PageNetworkUtils { } } -class SegmentAPIRequestBuffer { +export class TrackingAPIRequestBuffer { private requests: Request[] = [] - public lastEvent() { - return this.getEvents()[this.getEvents.length - 1] + public lastEvent(): SegmentEvent { + const allEvents = this.getEvents() + return allEvents[allEvents.length - 1] } public getEvents(): SegmentEvent[] { - return this.requests.flatMap((req) => req.postDataJSON().batch) + return this.requests.flatMap((req) => { + const body = req.postDataJSON() + return 'batch' in body ? body.batch : [body] + }) } clear() { this.requests = [] } + addRequest(request: Request) { if (request.method().toLowerCase() !== 'post') { throw new Error( @@ -135,18 +139,15 @@ class SegmentAPIRequestBuffer { } } -export class SignalAPIRequestBuffer extends SegmentAPIRequestBuffer { - /** - * @example 'network', 'interaction', 'navigation', etc - */ - override getEvents(signalType?: string): SegmentEvent[] { +export class SignalAPIRequestBuffer extends TrackingAPIRequestBuffer { + override getEvents(signalType?: Signal['type']): SegmentEvent[] { if (signalType) { return this.getEvents().filter((e) => e.properties!.type === signalType) } return super.getEvents() } - override lastEvent(signalType?: string | undefined): SegmentEvent { + override lastEvent(signalType?: Signal['type']): SegmentEvent { if (signalType) { const res = this.getEvents(signalType)[this.getEvents(signalType).length - 1] diff --git a/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts b/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts new file mode 100644 index 000000000..d8335fd77 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts @@ -0,0 +1,51 @@ +import { Page } from '@playwright/test' +import type { Compute } from './ts' + +export function waitForCondition( + conditionFn: () => boolean, + { + checkInterval = 100, + timeout = 10000, + errorMessage = 'Condition was not met within the specified time.', + } = {} +): Promise { + return new Promise((resolve, reject) => { + const startTime = Date.now() + + const interval = setInterval(() => { + try { + if (conditionFn()) { + clearInterval(interval) + resolve() + } else if (Date.now() - startTime >= timeout) { + clearInterval(interval) + reject(new Error(errorMessage)) + } + } catch (error) { + clearInterval(interval) + reject(error) + } + }, checkInterval) + }) +} + +type FillOptions = Compute[2]> + +export async function fillAndBlur( + page: Page, + selector: string, + text: string, + options: FillOptions = {} +) { + await page.fill(selector, text, options) + // Remove focus so the onChange event is triggered + await page.evaluate( + (args) => { + const input = document.querySelector(args.selector) as HTMLElement + if (input) { + input.blur() + } + }, + { selector } + ) +} diff --git a/packages/signals/signals-integration-tests/src/helpers/ts.ts b/packages/signals/signals-integration-tests/src/helpers/ts.ts new file mode 100644 index 000000000..b68acdfa9 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/helpers/ts.ts @@ -0,0 +1 @@ +export type Compute = { [K in keyof T]: T[K] } & {} diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts index 0aa145d88..9684fa2eb 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts @@ -54,7 +54,9 @@ test('Segment events', async ({ page }) => { indexPage.waitForTrackingApiFlush(), ]) - const trackingApiReqs = indexPage.trackingApiReqs.map(normalizeSnapshotEvent) + const trackingApiReqs = indexPage.trackingAPI + .getEvents() + .map(normalizeSnapshotEvent) expect(trackingApiReqs).toEqual(snapshot) }) @@ -76,13 +78,13 @@ test('Should dispatch events from signals that occurred before analytics was ins // add a user defined signal before analytics is instantiated void indexPage.addUserDefinedSignal() - await indexPage.waitForSignalsAssets() + await indexPage.waitForSettings() await Promise.all([ indexPage.waitForSignalsApiFlush(), indexPage.waitForTrackingApiFlush(), ]) - const trackingApiReqs = indexPage.trackingApiReqs + const trackingApiReqs = indexPage.trackingAPI.getEvents() expect(trackingApiReqs).toHaveLength(2) const pageEvents = trackingApiReqs.find((el) => el.type === 'page')! diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts index abed5e94a..833047649 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts @@ -124,8 +124,7 @@ test('interaction signals', async () => { }, }) - const analyticsReqJSON = indexPage.lastTrackingApiReq.postDataJSON() - + const analyticsReqJSON = indexPage.trackingAPI.lastEvent() expect(analyticsReqJSON).toMatchObject({ writeKey: '', event: 'click [interaction]', diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts index 392104cc5..2be92fede 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts @@ -3,15 +3,33 @@ import { IndexPage } from './index-page' const indexPage = new IndexPage() -const basicEdgeFn = ` - // this is a process signal function - const processSignal = (signal) => {}` +const basicEdgeFn = `const processSignal = (signal) => {}` test.beforeEach(async ({ page }) => { await indexPage.loadAndWait(page, basicEdgeFn) }) -test('button click (complex, with nested items)', async () => { +const data = { + eventType: 'click', + target: { + attributes: { + id: 'complex-button', + }, + classList: [], + id: 'complex-button', + labels: [], + name: '', + nodeName: 'BUTTON', + tagName: 'BUTTON', + title: '', + type: 'submit', + innerText: 'Other Example Button with Nested Text', + textContent: 'Other Example Button with Nested Text', + value: '', + }, +} + +test('clicking a button with nested content', async () => { /** * Click a button with nested text, ensure that that correct text shows up */ @@ -22,28 +40,28 @@ test('button click (complex, with nested items)', async () => { const interactionSignals = indexPage.signalsAPI.getEvents('interaction') expect(interactionSignals).toHaveLength(1) - const data = { - eventType: 'click', - target: { - attributes: { - id: 'complex-button', - }, - classList: [], - id: 'complex-button', - labels: [], - name: '', - nodeName: 'BUTTON', - tagName: 'BUTTON', - title: '', - type: 'submit', - innerText: expect.any(String), - textContent: expect.stringContaining( - 'Other Example Button with Nested Text' - ), - value: '', + + expect(interactionSignals[0]).toMatchObject({ + event: 'Segment Signal Generated', + type: 'track', + properties: { + type: 'interaction', + data, }, - } + }) +}) + +test('clicking the h1 tag inside a button', async () => { + /** + * Click the nested text, ensure that that correct text shows up + */ + await Promise.all([ + indexPage.clickInsideComplexButton(), + indexPage.waitForSignalsApiFlush(), + ]) + const interactionSignals = indexPage.signalsAPI.getEvents('interaction') + expect(interactionSignals).toHaveLength(1) expect(interactionSignals[0]).toMatchObject({ event: 'Segment Signal Generated', type: 'track', diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts new file mode 100644 index 000000000..0d301ce79 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test' +import { waitForCondition } from '../../helpers/playwright-utils' +import { IndexPage } from './index-page' + +const indexPage = new IndexPage() + +const basicEdgeFn = `const processSignal = (signal) => {}` + +test('Collecting signals whenever a user enters text input', async ({ + page, +}) => { + /** + * Input some text into the input field, see if the signal is emitted correctly + */ + await indexPage.loadAndWait(page, basicEdgeFn, { + disableSignalsRedaction: true, + }) + + await Promise.all([ + indexPage.fillNameInput('John Doe'), + indexPage.waitForSignalsApiFlush(), + ]) + + await waitForCondition( + () => indexPage.signalsAPI.getEvents('interaction').length > 0, + { errorMessage: 'No interaction signals found' } + ) + const interactionSignals = indexPage.signalsAPI.getEvents('interaction') + + const data = { + eventType: 'change', + target: expect.objectContaining({ + attributes: expect.objectContaining({ + type: 'text', + id: 'name', + name: 'name', + }), + classList: [], + id: 'name', + labels: [ + { + textContent: 'Name:', + }, + ], + name: 'name', + nodeName: 'INPUT', + tagName: 'INPUT', + value: 'John Doe', + }), + } + expect(interactionSignals[0].properties!.data).toMatchObject(data) +}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts index 2b4b67877..99b82ee39 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts @@ -1,5 +1,6 @@ import { BasePage } from '../../helpers/base-page-object' import { promiseTimeout } from '@internal/test-helpers' +import { fillAndBlur } from '../../helpers/playwright-utils' export class IndexPage extends BasePage { constructor() { @@ -40,4 +41,12 @@ export class IndexPage extends BasePage { async clickComplexButton() { return this.page.click('#complex-button') } + + async clickInsideComplexButton() { + return this.page.click('#complex-button h1') + } + + async fillNameInput(text: string) { + return await fillAndBlur(this.page, '#name', text) + } } diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-filter.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-filter.test.ts index 21107dd7c..38dbf24d8 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-filter.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-filter.test.ts @@ -3,14 +3,7 @@ import { IndexPage } from './index-page' const indexPage = new IndexPage() -const basicEdgeFn = ` - // this is a process signal function - const processSignal = (signal) => { - if (signal.type === 'interaction') { - const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' - analytics.track(eventName, signal.data) - } - }` +const basicEdgeFn = `const processSignal = (signal) => {}` test('network signals allow and disallow list', async ({ page }) => { await indexPage.loadAndWait(page, basicEdgeFn, { diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts index ec85f761e..d2e63a9ac 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts @@ -1,14 +1,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const basicEdgeFn = ` - // this is a process signal function - const processSignal = (signal) => { - if (signal.type === 'interaction') { - const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' - analytics.track(eventName, signal.data) - } - }` +const basicEdgeFn = `const processSignal = (signal) => {}` test.describe('XHR Tests', () => { let indexPage: IndexPage @@ -161,6 +154,7 @@ test.describe('XHR Tests', () => { }) await indexPage.network.makeXHRCall('http://localhost/test', { + responseType: 'json', method: 'GET', }) @@ -269,5 +263,4 @@ test.describe('XHR Tests', () => { data: { req2: 'value' }, }) }) - // Check the response }) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts new file mode 100644 index 000000000..6540078bc --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' +import { waitForCondition } from '../../helpers/playwright-utils' +import { IndexPage } from './index-page' + +const indexPage = new IndexPage() + +const basicEdgeFn = `const processSignal = (signal) => {}` + +test('redaction enabled -> will XXX the value of text input', async ({ + page, +}) => { + await indexPage.loadAndWait(page, basicEdgeFn, { + disableSignalsRedaction: false, + }) + + await Promise.all([ + indexPage.fillNameInput('John Doe'), + indexPage.waitForSignalsApiFlush(), + ]) + + await waitForCondition( + () => indexPage.signalsAPI.getEvents('interaction').length > 0, + { errorMessage: 'No interaction signals found' } + ) + const interactionSignals = indexPage.signalsAPI.getEvents('interaction') + + const data = { + eventType: 'change', + target: expect.objectContaining({ + name: 'name', + type: 'text', + value: 'XXX', // redacted + }), + } + expect(interactionSignals[0].properties!.data).toMatchObject(data) +}) + +test('redation disabled -> will not touch the value of text input', async ({ + page, +}) => { + await indexPage.loadAndWait(page, basicEdgeFn, { + disableSignalsRedaction: true, + }) + + await Promise.all([ + indexPage.fillNameInput('John Doe'), + indexPage.waitForSignalsApiFlush(), + ]) + + await waitForCondition( + () => indexPage.signalsAPI.getEvents('interaction').length > 0, + { errorMessage: 'No interaction signals found' } + ) + const interactionSignals = indexPage.signalsAPI.getEvents('interaction') + + const data = { + eventType: 'change', + target: expect.objectContaining({ + value: 'John Doe', // noe redacted + }), + } + expect(interactionSignals[0].properties!.data).toMatchObject(data) +}) diff --git a/packages/signals/signals/src/core/client/__tests__/client.test.ts b/packages/signals/signals/src/core/client/__tests__/client.test.ts index b008f725a..11255dd92 100644 --- a/packages/signals/signals/src/core/client/__tests__/client.test.ts +++ b/packages/signals/signals/src/core/client/__tests__/client.test.ts @@ -15,7 +15,7 @@ describe(SignalsIngestClient, () => { await client.init({ writeKey: 'test' }) }) - it('makes a track call via the analytics api', async () => { + it('makes an instrumentation track call via the analytics api', async () => { expect(client).toBeTruthy() const ctx = await client.send({ type: 'instrumentation', @@ -32,9 +32,36 @@ describe(SignalsIngestClient, () => { index: 0, data: { rawEvent: { - foo: 'XXX', + foo: 'bar', }, }, }) }) + it('makes a network track call via the analytics api', async () => { + expect(client).toBeTruthy() + const ctx = await client.send({ + type: 'network', + data: { + action: 'request', + data: { + hello: 'how are you', + }, + method: 'post', + url: 'http://foo.com', + }, + }) + + expect(ctx!.event.type).toEqual('track') + expect(ctx!.event.properties!.type).toBe('network') + expect(ctx!.event.properties!.data).toMatchInlineSnapshot(` + { + "action": "request", + "data": { + "hello": "XXX", + }, + "method": "post", + "url": "http://foo.com", + } + `) + }) }) diff --git a/packages/signals/signals/src/core/client/__tests__/redact.test.ts b/packages/signals/signals/src/core/client/__tests__/redact.test.ts index 1f7c814cb..c205a5031 100644 --- a/packages/signals/signals/src/core/client/__tests__/redact.test.ts +++ b/packages/signals/signals/src/core/client/__tests__/redact.test.ts @@ -1,6 +1,12 @@ -import { redactJsonValues } from '../redact' +import { + createInstrumentationSignal, + createInteractionSignal, + createNetworkSignal, + NetworkSignalMetadata, +} from '../../../types' +import { redactJsonValues, redactSignalData } from '../redact' -describe('redactJsonValues', () => { +describe(redactJsonValues, () => { it('should redact string values in an object', () => { const obj = { name: 'John Doe', age: '30' } const expected = { name: 'XXX', age: 'XXX' } @@ -54,3 +60,68 @@ describe('redactJsonValues', () => { expect(redactJsonValues(obj, 3)).toEqual(expected) }) }) + +describe(redactSignalData, () => { + const metadataFixture: NetworkSignalMetadata = { + filters: { + allowed: [], + disallowed: [], + }, + } + it('should return the signal as is if the type is "instrumentation"', () => { + const signal = createInstrumentationSignal({ + foo: 123, + } as any) + expect(redactSignalData(signal)).toEqual(signal) + }) + + it('should return the signal as is if the type is "userDefined"', () => { + const signal = { type: 'userDefined', data: { value: 'secret' } } as const + expect(redactSignalData(signal)).toEqual(signal) + }) + + it('should redact the value in the "target" property if the type is "interaction"', () => { + const signal = createInteractionSignal({ + eventType: 'change', + target: { value: 'secret' }, + }) + const expected = createInteractionSignal({ + eventType: 'change', + target: { value: 'XXX' }, + }) + expect(redactSignalData(signal)).toEqual(expected) + }) + + it('should redact the values in the "data" property if the type is "network"', () => { + const signal = createNetworkSignal( + { + action: 'request', + method: 'post', + url: 'http://foo.com', + data: { name: 'John Doe', age: 30 }, + }, + metadataFixture + ) + const expected = createNetworkSignal( + { + action: 'request', + method: 'post', + url: 'http://foo.com', + data: { name: 'XXX', age: 999 }, + }, + metadataFixture + ) + expect(redactSignalData(signal)).toEqual(expected) + }) + + it('should not mutate the original signal object', () => { + const originalSignal = createInteractionSignal({ + eventType: 'click', + target: { value: 'sensitiveData' }, + }) + const originalSignalCopy = JSON.parse(JSON.stringify(originalSignal)) + + redactSignalData(originalSignal) + expect(originalSignal).toEqual(originalSignalCopy) + }) +}) diff --git a/packages/signals/signals/src/core/client/index.ts b/packages/signals/signals/src/core/client/index.ts index 12ff53d57..346c2c324 100644 --- a/packages/signals/signals/src/core/client/index.ts +++ b/packages/signals/signals/src/core/client/index.ts @@ -1,7 +1,7 @@ import { Analytics, segmentio } from '@segment/analytics-next' import { logger } from '../../lib/logger' import { Signal } from '../../types' -import { redactJsonValues } from './redact' +import { redactSignalData } from './redact' export class SignalsIngestSettings { flushAt: number @@ -74,12 +74,10 @@ export class SignalsIngestClient { throw new Error('Please initialize before calling this method.') } const disableRedaction = this.settings.shouldDisableSignalRedaction() - const data = disableRedaction - ? signal.data - : redactJsonValues(signal.data, 2) + const cleanSignal = disableRedaction ? signal : redactSignalData(signal) if (disableRedaction) { - logger.debug('Sending unredacted data to segment', data) + logger.debug('Sending unredacted data to segment', cleanSignal) } const MAGIC_EVENT_NAME = 'Segment Signal Generated' @@ -87,7 +85,7 @@ export class SignalsIngestClient { return this.analytics.track(MAGIC_EVENT_NAME, { index: this.index++, type: signal.type, - data: data, + data: cleanSignal.data, }) } diff --git a/packages/signals/signals/src/core/client/redact.ts b/packages/signals/signals/src/core/client/redact.ts index eeb5e0d29..dd861ee62 100644 --- a/packages/signals/signals/src/core/client/redact.ts +++ b/packages/signals/signals/src/core/client/redact.ts @@ -1,4 +1,18 @@ -function redact(value: unknown) { +import { Signal } from '../../types' + +export const redactSignalData = (signalArg: Signal): Signal => { + const signal = structuredClone(signalArg) + if (signal.type === 'interaction') { + if ('target' in signal.data && 'value' in signal.data.target) { + signal.data.target.value = redactJsonValues(signal.data.target.value) + } + } else if (signal.type === 'network') { + signal.data = redactJsonValues(signal.data, 2) + } + return signal +} + +function redactPrimitive(value: unknown) { const type = typeof value if (type === 'boolean') { return true @@ -31,7 +45,7 @@ export function redactJsonValues(data: unknown, redactAfterDepth = 0): any { return redactedData } } else if (redactAfterDepth <= 0) { - const ret = redact(data) + const ret = redactPrimitive(data) return ret } else { return data diff --git a/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts b/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts index cb0a00cc2..dda0ae77a 100644 --- a/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts +++ b/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts @@ -15,7 +15,7 @@ describe('SignalsRuntime', () => { beforeEach(() => { signal1 = createInstrumentationSignal({ type: 'track' }) signal2 = createInteractionSignal({ eventType: 'submit', submitter: {} }) - signal3 = createInteractionSignal({ eventType: 'change' }) + signal3 = createInteractionSignal({ eventType: 'change', target: {} }) signalsRuntime = createSignalsRuntime([signal1, signal2, signal3]) }) diff --git a/packages/signals/signals/src/types/signals.ts b/packages/signals/signals/src/types/signals.ts index ffb5bbc37..6e62d952f 100644 --- a/packages/signals/signals/src/types/signals.ts +++ b/packages/signals/signals/src/types/signals.ts @@ -35,7 +35,7 @@ type SubmitData = { type ChangeData = { eventType: 'change' - [key: string]: unknown + target: SerializedTarget } export type InteractionSignal = AppSignal<'interaction', InteractionData>