diff --git a/.changeset/pink-ladybugs-impress.md b/.changeset/pink-ladybugs-impress.md new file mode 100644 index 000000000..153bd0f15 --- /dev/null +++ b/.changeset/pink-ladybugs-impress.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-signals': minor +'@segment/analytics-signals-runtime': minor +--- + +Add page data to web signals diff --git a/packages/signals/signals-integration-tests/src/helpers/fixtures.ts b/packages/signals/signals-integration-tests/src/helpers/fixtures.ts new file mode 100644 index 000000000..58494cafe --- /dev/null +++ b/packages/signals/signals-integration-tests/src/helpers/fixtures.ts @@ -0,0 +1,15 @@ +import { PageData } from '@segment/analytics-signals-runtime' + +const pageData: PageData = { + hash: '', + hostname: 'localhost', + path: '/src/tests/signals-vanilla/index.html', + referrer: '', + search: '', + title: '', + url: 'http://localhost:5432/src/tests/signals-vanilla/index.html', +} + +export const commonSignalData = { + page: pageData, +} 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 76fd4aa4f..cbb0993ff 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 @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test' +import { commonSignalData } from '../../helpers/fixtures' import { IndexPage } from './index-page' const basicEdgeFn = ` @@ -57,6 +58,7 @@ test('network signals xhr', async () => { ) expect(responses).toHaveLength(1) expect(responses[0].properties!.data.data).toEqual({ someResponse: 'yep' }) + expect(responses[0].properties!.data.page).toEqual(commonSignalData.page) }) test('instrumentation signals', async () => { @@ -113,6 +115,7 @@ test('interaction signals', async () => { type: 'submit', value: '', }, + page: commonSignalData.page, } expect(interactionSignals[0]).toMatchObject({ @@ -182,6 +185,7 @@ test('navigation signals', async ({ page }) => { hash: '#foo', search: '', title: '', + page: expect.any(Object), }, }) } 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 40a4bbb8c..a1a7f3220 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 @@ -27,11 +27,8 @@ export class IndexPage extends BasePage { return this.page.evaluate( (args) => { window.signalsPlugin.addSignal({ - type: 'userDefined', - data: { - foo: 'bar', - ...args.data, - }, + foo: 'bar', + ...args.data, }) }, { data } diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts index 6c833719e..868b089d4 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test' +import { commonSignalData } from '../../helpers/fixtures' import { IndexPage } from './index-page' const basicEdgeFn = `const processSignal = (signal) => {}` @@ -32,6 +33,7 @@ test.describe('network signals - fetch', () => { data: null, method: 'POST', url: 'http://localhost/upload', + ...commonSignalData, }) }) @@ -61,6 +63,7 @@ test.describe('network signals - fetch', () => { action: 'request', url: 'http://localhost/test', data: { key: 'value' }, + ...commonSignalData, }) }) @@ -90,6 +93,7 @@ test.describe('network signals - fetch', () => { action: 'request', url: 'http://localhost/test', data: 'hello world', + ...commonSignalData, }) }) @@ -104,6 +108,7 @@ test.describe('network signals - fetch', () => { method: 'POST', body: JSON.stringify({ key: 'value' }), contentType: 'application/json', + ...commonSignalData, } ) @@ -124,6 +129,7 @@ test.describe('network signals - fetch', () => { url: 'http://localhost/test', method: 'POST', data: { key: 'value' }, + ...commonSignalData, }) const responses = networkEvents.filter( @@ -137,6 +143,7 @@ test.describe('network signals - fetch', () => { data: { foo: 'test' }, status: 200, ok: true, + ...commonSignalData, }) }) @@ -165,6 +172,7 @@ test.describe('network signals - fetch', () => { action: 'request', url: `${indexPage.origin()}/test`, data: { key: 'value' }, + ...commonSignalData, }) const responses = networkEvents.filter( @@ -175,6 +183,7 @@ test.describe('network signals - fetch', () => { action: 'response', url: `${indexPage.origin()}/test`, data: { foo: 'test' }, + ...commonSignalData, }) }) @@ -214,6 +223,7 @@ test.describe('network signals - fetch', () => { data: { errorMsg: 'foo' }, status: 400, ok: false, + page: expect.any(Object), }) expect(responses).toHaveLength(1) }) @@ -253,6 +263,7 @@ test.describe('network signals - fetch', () => { data: 'foo', status: 400, ok: false, + ...commonSignalData, }) expect(responses).toHaveLength(1) }) diff --git a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts index bf3ce6361..7d9024b89 100644 --- a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts +++ b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts @@ -2,6 +2,7 @@ import { InstrumentationSignal, InteractionSignal, Signal } from '../index' import { mockInstrumentationSignal, mockInteractionSignal, + mockPageData, } from '../test-helpers/mocks/mock-signal-types-web' import { WebSignalsRuntime } from '../web/web-signals-runtime' @@ -17,6 +18,7 @@ describe(WebSignalsRuntime, () => { signal3 = { ...mockInteractionSignal, data: { + page: mockPageData, eventType: 'change', target: {} as any, change: {}, diff --git a/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts index 08a19988a..80b7161e4 100644 --- a/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts +++ b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts @@ -4,11 +4,24 @@ import { InstrumentationSignal, UserDefinedSignal, NetworkSignal, + PageData, } from '../../web/web-signals-types' // Mock data for testing + +export const mockPageData: PageData = { + url: 'https://www.segment.com/docs/connections/sources/catalog/libraries/website/javascript/', + path: '/docs/connections/sources/catalog/libraries/website/javascript/', + search: '', + hostname: 'www.segment.com', + hash: '', + referrer: '', + title: 'Segment - Documentation', +} + export const mockInteractionSignal: InteractionSignal = { type: 'interaction', data: { + page: mockPageData, eventType: 'click', target: { id: 'button1', @@ -22,6 +35,7 @@ export const mockInteractionSignal: InteractionSignal = { export const mockNavigationSignal: NavigationSignal = { type: 'navigation', data: { + page: mockPageData, action: 'urlChange', url: 'https://example.com', hash: '#section1', @@ -33,6 +47,7 @@ export const mockNavigationSignal: NavigationSignal = { export const mockInstrumentationSignal: InstrumentationSignal = { type: 'instrumentation', data: { + page: mockPageData, rawEvent: { type: 'customEvent', detail: 'example' }, }, metadata: { timestamp: Date.now() }, @@ -41,6 +56,7 @@ export const mockInstrumentationSignal: InstrumentationSignal = { export const mockNetworkSignal: NetworkSignal = { type: 'network', data: { + page: mockPageData, action: 'request', contentType: 'application/json', url: 'https://api.example.com/data', @@ -53,6 +69,7 @@ export const mockNetworkSignal: NetworkSignal = { export const mockUserDefinedSignal: UserDefinedSignal = { type: 'userDefined', data: { + page: mockPageData, customField: 'customValue', }, metadata: { timestamp: Date.now() }, diff --git a/packages/signals/signals-runtime/src/web/web-signals-types.ts b/packages/signals/signals-runtime/src/web/web-signals-types.ts index 37baf42b4..d5a17b569 100644 --- a/packages/signals/signals-runtime/src/web/web-signals-types.ts +++ b/packages/signals/signals-runtime/src/web/web-signals-types.ts @@ -2,9 +2,55 @@ import { BaseSignal, JSONValue } from '../shared/shared-types' export type SignalTypes = Signal['type'] +export interface PageData { + /** + * The full URL of the page + * If there is a canonical URL, this should be the canonical URL + * @example https://www.segment.com/docs/connections/sources/catalog/libraries/website/javascript/ + */ + url: string + /** + * The path of the page + * @example /docs/connections/sources/catalog/libraries/website/javascript/ + */ + path: string + /** + * The search parameters of the page + * @example ?utm_source=google + */ + search: string + /** + * The hostname of the page + * @example www.segment.com + */ + hostname: string + /** + * The hash of the page + * @example #hash + */ + hash: string + /** + * The referrer of the page + * @example https://www.google.com/ + */ + referrer: string + /** + * The title of the page + * @example Segment - Documentation + */ + title: string +} + +/** + * The base data that all web signal data must have + */ +export interface BaseWebData { + page: PageData +} + export interface RawSignal extends BaseSignal { type: T - data: Data + data: BaseWebData & Data metadata?: Record } export type InteractionData = ClickData | SubmitData | ChangeData diff --git a/packages/signals/signals/README.md b/packages/signals/signals/README.md index 8049046d4..391fd0a02 100644 --- a/packages/signals/signals/README.md +++ b/packages/signals/signals/README.md @@ -59,10 +59,13 @@ analytics.load({ ```ts import { signalsPlugin } from './analytics' // assuming you exported your plugin instance. -signalsPlugin.addSignal({ - type: 'userDefined', - data: { foo: 'bar' } -}) +signalsPlugin.addSignal({ someData: 'foo' }) + +// emits a signal with the following shape +{ + type: 'userDefined' + data: { someData: 'foo', ... } +} ``` ### Debugging diff --git a/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts b/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts index 483dc0055..f72e2056f 100644 --- a/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts +++ b/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts @@ -38,20 +38,20 @@ describe(AnalyticsService, () => { middleware({ payload: { obj: mockEvent }, next: jest.fn() }) expect(mockSignalEmitter.emit).toHaveBeenCalledTimes(1) - expect(mockSignalEmitter.emit.mock.calls[0]).toMatchInlineSnapshot(` - [ - { - "data": { - "rawEvent": { - "context": { - "foo": 123, - }, - "type": "track", + const call = mockSignalEmitter.emit.mock.calls[0][0] + delete call.data.page + expect(call).toMatchInlineSnapshot(` + { + "data": { + "rawEvent": { + "context": { + "foo": 123, }, + "type": "track", }, - "type": "instrumentation", }, - ] + "type": "instrumentation", + } `) }) it('should not emit signals if the event is a Signal event', async () => { diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts index 703442ef8..aab8c3c0e 100644 --- a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts +++ b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts @@ -1,5 +1,10 @@ import { createSuccess } from '@segment/analytics-next/src/test-helpers/factories' import unfetch from 'unfetch' +import { getPageData } from '../../../../lib/page-data' +import { + createInstrumentationSignal, + createNetworkSignal, +} from '../../../../types/factories' import { SignalsIngestClient } from '../signals-ingest-client' jest.mock('unfetch') @@ -18,14 +23,10 @@ describe(SignalsIngestClient, () => { it('makes an instrumentation track call via the analytics api', async () => { expect(client).toBeTruthy() - const ctx = await client.send({ - type: 'instrumentation', - data: { - rawEvent: { - foo: 'bar', - }, - }, + const signal = createInstrumentationSignal({ + type: 'track', }) + const ctx = await client.send(signal) expect(ctx!.event.type).toEqual('track') expect(ctx!.event.properties).toEqual({ @@ -33,26 +34,25 @@ describe(SignalsIngestClient, () => { index: 0, data: { rawEvent: { - foo: 'bar', + type: 'track', }, + page: getPageData(), }, }) }) it('makes a network track call via the analytics api', async () => { expect(client).toBeTruthy() - const ctx = await client.send({ - type: 'network', + const signal = createNetworkSignal({ + contentType: 'application/json', + action: 'request', data: { - contentType: 'application/json', - action: 'request', - data: { - hello: 'how are you', - }, - method: 'POST', - url: 'http://foo.com', + hello: 'how are you', }, + method: 'POST', + url: 'http://foo.com', }) + const ctx = await client.send(signal) expect(ctx!.event.type).toEqual('track') expect(ctx!.event.properties!.type).toBe('network') expect(ctx!.event.properties!.data).toMatchInlineSnapshot(` @@ -63,6 +63,15 @@ describe(SignalsIngestClient, () => { "hello": "XXX", }, "method": "POST", + "page": { + "hash": "", + "hostname": "localhost", + "path": "/", + "referrer": "", + "search": "", + "title": "", + "url": "http://localhost/", + }, "url": "http://foo.com", } `) diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts index c094e77e8..7b928b403 100644 --- a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts +++ b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts @@ -73,7 +73,7 @@ describe(redactSignalData, () => { }) it('should return the signal as is if the type is "userDefined"', () => { - const signal = { type: 'userDefined', data: { value: 'secret' } } as const + const signal = factories.createUserDefinedSignal({ value: 'secret' }) expect(redactSignalData(signal)).toEqual(signal) }) diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts b/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts index b6bc60938..b7f888419 100644 --- a/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts +++ b/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts @@ -72,7 +72,7 @@ export const redactSignalData = (signalArg: Signal): Signal => { } } } else if (signal.type === 'network') { - signal.data = redactJsonValues(signal.data, 2) + signal.data.data = redactJsonValues(signal.data.data) } return signal } diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts index bb7da3533..32dfee45a 100644 --- a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts +++ b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts @@ -104,6 +104,15 @@ describe(NetworkGenerator, () => { "key": "value", }, "method": "POST", + "page": { + "hash": "", + "hostname": "localhost", + "path": "/", + "referrer": "", + "search": "", + "title": "", + "url": "http://localhost/", + }, "url": "http://localhost/test", }, "metadata": { @@ -125,6 +134,15 @@ describe(NetworkGenerator, () => { "data": "test", }, "ok": true, + "page": { + "hash": "", + "hostname": "localhost", + "path": "/", + "referrer": "", + "search": "", + "title": "", + "url": "http://localhost/", + }, "status": 200, "url": "http://localhost/test", }, diff --git a/packages/signals/signals/src/lib/page-data/index.ts b/packages/signals/signals/src/lib/page-data/index.ts new file mode 100644 index 000000000..6c3235d75 --- /dev/null +++ b/packages/signals/signals/src/lib/page-data/index.ts @@ -0,0 +1,13 @@ +import { PageData } from '@segment/analytics-signals-runtime' + +export const getPageData = (): PageData => { + return { + path: location.pathname, + referrer: document.referrer, + title: document.title, + search: location.search, + url: location.href, + hostname: location.hostname, + hash: location.hash, + } +} diff --git a/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts b/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts index cd1983b5a..e28d336b7 100644 --- a/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts +++ b/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts @@ -1,3 +1,4 @@ +import { createUserDefinedSignal } from '../../types/factories' import { SignalsPlugin } from '../signals-plugin' // this specific test was throwing a bunch of warnings: @@ -41,10 +42,12 @@ describe(SignalsPlugin, () => { test('addSignal method emits signal', async () => { const plugin = new SignalsPlugin() - const signal = { data: 'test' } as any const emitterSpy = jest.spyOn(plugin.signals.signalEmitter, 'emit') - plugin.addSignal(signal) + const userDefinedData = { foo: 'bar' } + plugin.addSignal(userDefinedData) expect(emitterSpy).toHaveBeenCalledTimes(1) - expect(emitterSpy.mock.calls[0][0]).toEqual({ data: 'test' }) + expect(emitterSpy.mock.calls[0][0]).toEqual( + createUserDefinedSignal(userDefinedData) + ) }) }) diff --git a/packages/signals/signals/src/plugin/signals-plugin.ts b/packages/signals/signals/src/plugin/signals-plugin.ts index 647269e70..946a70a9f 100644 --- a/packages/signals/signals/src/plugin/signals-plugin.ts +++ b/packages/signals/signals/src/plugin/signals-plugin.ts @@ -5,6 +5,7 @@ import { AnyAnalytics, SignalsPluginSettingsConfig } from '../types' import { Signal } from '@segment/analytics-signals-runtime' import { assertBrowserEnv } from '../lib/assert-browser-env' import { version } from '../generated/version' +import { createUserDefinedSignal } from '../types/factories' export type OnSignalCb = (signal: Signal) => void @@ -16,9 +17,9 @@ interface SignalsAugmentedFunctionality { onSignal: (fn: OnSignalCb) => this /** - * Emit/add a custom signal + * Emit/add a custom signal of type 'userDefined' */ - addSignal(data: Signal): this + addSignal(userDefinedData: Record): this } export class SignalsPlugin implements Plugin, SignalsAugmentedFunctionality { @@ -82,8 +83,8 @@ export class SignalsPlugin implements Plugin, SignalsAugmentedFunctionality { return this } - addSignal(signal: Signal): this { - this.signals.signalEmitter.emit(signal) + addSignal(data: Record): this { + this.signals.signalEmitter.emit(createUserDefinedSignal(data)) return this } diff --git a/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts b/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts index 374f2f960..150020d2d 100644 --- a/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts +++ b/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts @@ -37,6 +37,15 @@ describe(createNetworkSignal, () => { "key": "value", }, "method": "POST", + "page": { + "hash": "", + "hostname": "localhost", + "path": "/", + "referrer": "", + "search": "", + "title": "", + "url": "http://localhost/", + }, "url": "http://example.com", }, "metadata": { @@ -78,6 +87,15 @@ describe(createNetworkSignal, () => { "key": "value", }, "ok": true, + "page": { + "hash": "", + "hostname": "localhost", + "path": "/", + "referrer": "", + "search": "", + "title": "", + "url": "http://localhost/", + }, "status": 200, "url": "http://example.com", }, diff --git a/packages/signals/signals/src/types/factories.ts b/packages/signals/signals/src/types/factories.ts index 55b577616..5abd4e1fd 100644 --- a/packages/signals/signals/src/types/factories.ts +++ b/packages/signals/signals/src/types/factories.ts @@ -12,6 +12,7 @@ import { SegmentEvent, } from '@segment/analytics-signals-runtime' import { normalizeUrl } from '../lib/normalize-url' +import { getPageData } from '../lib/page-data' /** * Factories @@ -22,6 +23,7 @@ export const createInstrumentationSignal = ( return { type: 'instrumentation', data: { + page: getPageData(), rawEvent, }, } @@ -32,7 +34,10 @@ export const createInteractionSignal = ( ): InteractionSignal => { return { type: 'interaction', - data, + data: { + ...data, + page: getPageData(), + }, } } @@ -41,7 +46,10 @@ export const createNavigationSignal = ( ): NavigationSignal => { return { type: 'navigation', - data, + data: { + ...data, + page: getPageData(), + }, } } @@ -50,7 +58,10 @@ export const createUserDefinedSignal = ( ): UserDefinedSignal => { return { type: 'userDefined', - data, + data: { + ...data, + page: getPageData(), + }, } } @@ -63,6 +74,7 @@ export const createNetworkSignal = ( data: { ...data, url: normalizeUrl(data.url), + page: getPageData(), }, metadata: metadata ?? { filters: {