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..d8810e52f 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..dfda6aea5 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,11 @@ -import { redactJsonValues } from '../redact' +import { + createInstrumentationSignal, + createInteractionSignal, + createNetworkSignal, +} 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 +59,56 @@ describe('redactJsonValues', () => { expect(redactJsonValues(obj, 3)).toEqual(expected) }) }) + +describe(redactSignalData, () => { + 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 }, + }) + const expected = createNetworkSignal({ + action: 'Request', + method: 'post', + url: 'http://foo.com', + data: { name: 'XXX', age: 999 }, + }) + 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>