Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/late-bikes-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-signals': minor
'@segment/analytics-signals-runtime': minor
---

Add anonymousID and timestamp to signals
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ module.exports = () =>
'<rootDir>/packages/consent/consent-wrapper-onetrust',
'<rootDir>/scripts',
'<rootDir>/packages/signals/signals',
'<rootDir>/packages/test-helpers',
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { test, expect } from '@playwright/test'
import { waitForCondition } from '../../helpers/playwright-utils'
import { IndexPage } from './index-page'

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
if (signal.type === 'interaction') {
analytics.track('hello', { myAnonId: signal.anonymousId, myTimestamp: signal.timestamp })
}
}
`

let indexPage: IndexPage

const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/

test.beforeEach(async ({ page }) => {
indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
})

test('Signals should have anonymousId and timestamp at top level', async () => {
await indexPage.network.mockTestRoute()
await indexPage.network.makeFetchCall()
await Promise.all([
indexPage.clickButton(),
indexPage.makeAnalyticsPageCall(),
indexPage.waitForSignalsApiFlush(),
indexPage.waitForTrackingApiFlush(),
])

const types = [
'network',
'interaction',
'instrumentation',
'navigation',
] as const

const evs = types.map((type) => ({
type,
networkCalls: indexPage.signalsAPI.getEvents(type),
}))

evs.forEach((events) => {
if (!events.networkCalls.length) {
throw new Error(`No events found for type ${events.type}`)
}
events.networkCalls.forEach((event) => {
const expected = {
anonymousId: event.anonymousId,
timestamp: expect.stringMatching(isoDateRegEx),
type: event.properties!.type,
}
expect(event.properties).toMatchObject(expected)
})
})

const getCreatedEvent = () =>
indexPage.trackingAPI
.getEvents()
.find((el) => el.type === 'track' && el.event === 'hello')

await waitForCondition(() => !!getCreatedEvent(), {
errorMessage: 'No track events found, should have an event',
})
const event = getCreatedEvent()!
expect(event.properties).toEqual({
myAnonId: event.anonymousId,
myTimestamp: expect.stringMatching(isoDateRegEx),
})
})
2 changes: 1 addition & 1 deletion packages/signals/signals-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs",
"build:global": "node build-signals-runtime-global.js",
"assert-generated": "bash scripts/assert-generated.sh",
"watch": "rm -rf dist/esm && yarn build:esm --watch",
"watch": "rm -rf dist/esm && yarn build:esm && yarn build:esm --watch",
"watch:test": "yarn test --watch",
"tsc": "yarn run -T tsc",
"eslint": "yarn run -T eslint",
Expand Down
4 changes: 4 additions & 0 deletions packages/signals/signals-runtime/src/shared/shared-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export type ID = string | null | undefined

export interface BaseSignal {
type: string
anonymousId: ID
timestamp: string
}

export type SignalOfType<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BaseSignal } from '../../shared/shared-types'
import {
InteractionSignal,
NavigationSignal,
Expand All @@ -8,6 +9,13 @@ import {
} from '../../web/web-signals-types'
// Mock data for testing

type DefaultProps = Pick<BaseSignal, 'anonymousId' | 'timestamp'>

const baseSignalProps: DefaultProps = {
anonymousId: '123',
timestamp: '2020-01-01T00:00:00.000Z',
}

export const mockPageData: PageData = {
url: 'https://www.segment.com/docs/connections/sources/catalog/libraries/website/javascript/',
path: '/docs/connections/sources/catalog/libraries/website/javascript/',
Expand All @@ -29,7 +37,7 @@ export const mockInteractionSignal: InteractionSignal = {
attributes: { id: 'button1', class: 'btn-primary' },
},
},
metadata: { timestamp: Date.now() },
...baseSignalProps,
}

export const mockNavigationSignal: NavigationSignal = {
Expand All @@ -41,7 +49,7 @@ export const mockNavigationSignal: NavigationSignal = {
hash: '#section1',
prevUrl: 'https://example.com/home',
},
metadata: { timestamp: Date.now() },
...baseSignalProps,
}

export const mockInstrumentationSignal: InstrumentationSignal = {
Expand All @@ -50,7 +58,7 @@ export const mockInstrumentationSignal: InstrumentationSignal = {
page: mockPageData,
rawEvent: { type: 'customEvent', detail: 'example' },
},
metadata: { timestamp: Date.now() },
...baseSignalProps,
}

export const mockNetworkSignal: NetworkSignal = {
Expand All @@ -63,7 +71,7 @@ export const mockNetworkSignal: NetworkSignal = {
method: 'GET',
data: { key: 'value' },
},
metadata: { timestamp: Date.now() },
...baseSignalProps,
}

export const mockUserDefinedSignal: UserDefinedSignal = {
Expand All @@ -72,5 +80,5 @@ export const mockUserDefinedSignal: UserDefinedSignal = {
page: mockPageData,
customField: 'customValue',
},
metadata: { timestamp: Date.now() },
...baseSignalProps,
}
2 changes: 1 addition & 1 deletion packages/signals/signals/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ const { createJestTSConfig } = require('@internal/config')

module.exports = createJestTSConfig(__dirname, {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./jest.setup.js'],
setupFilesAfterEnv: ['./jest.setup.ts'],
})
3 changes: 0 additions & 3 deletions packages/signals/signals/jest.setup.js

This file was deleted.

6 changes: 6 additions & 0 deletions packages/signals/signals/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable no-undef */
import './src/test-helpers/jest-extended'
import { JestSerializers } from '@internal/test-helpers'
import 'fake-indexeddb/auto'
globalThis.structuredClone = (v: any) => JSON.parse(JSON.stringify(v))
expect.addSnapshotSerializer(JestSerializers.jestSnapshotSerializerTimestamp)
1 change: 1 addition & 0 deletions packages/signals/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
],
"scripts": {
".": "yarn run -T turbo run --filter=@segment/analytics-signals...",
"..": "yarn run -T turbo run --filter=...@segment/analytics-signals...",
"test": "yarn jest",
"lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'",
"build": "rm -rf dist && yarn concurrently 'yarn:build:*'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe(AnalyticsService, () => {
delete call.data.page
expect(call).toMatchInlineSnapshot(`
{
"anonymousId": "",
"data": {
"rawEvent": {
"context": {
Expand All @@ -50,6 +51,7 @@ describe(AnalyticsService, () => {
"type": "track",
},
},
"timestamp": <ISO Timestamp>,
"type": "instrumentation",
}
`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ISO_TIMESTAMP_REGEX } from '@internal/test-helpers'
import { createSuccess } from '@segment/analytics-next/src/test-helpers/factories'
import unfetch from 'unfetch'
import { getPageData } from '../../../../lib/page-data'
Expand Down Expand Up @@ -29,7 +30,9 @@ describe(SignalsIngestClient, () => {
const ctx = await client.send(signal)

expect(ctx!.event.type).toEqual('track')
expect(ctx!.event.properties).toEqual({
expect(ctx!.event.properties).toMatchObject({
anonymousId: expect.any(String),
timestamp: expect.stringMatching(ISO_TIMESTAMP_REGEX),
type: 'instrumentation',
index: 0,
data: {
Expand All @@ -53,26 +56,36 @@ describe(SignalsIngestClient, () => {
})

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(`
expect(ctx!.event.properties!).toMatchInlineSnapshot(`
{
"action": "request",
"contentType": "application/json",
"anonymousId": "",
"data": {
"hello": "XXX",
"action": "request",
"contentType": "application/json",
"data": {
"hello": "XXX",
},
"method": "POST",
"page": {
"hash": "",
"hostname": "localhost",
"path": "/",
"referrer": "",
"search": "",
"title": "",
"url": "http://localhost/",
},
"url": "http://foo.com",
},
"method": "POST",
"page": {
"hash": "",
"hostname": "localhost",
"path": "/",
"referrer": "",
"search": "",
"title": "",
"url": "http://localhost/",
"index": 0,
"metadata": {
"filters": {
"allowed": [],
"disallowed": [],
},
},
"url": "http://foo.com",
"timestamp": <ISO Timestamp>,
"type": "network",
}
`)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe(redactSignalData, () => {
listener: 'onchange',
target: createMockTarget({ value: 'XXX', formData: { password: 'XXX' } }),
})
expect(redactSignalData(signal)).toEqual(expected)
expect(redactSignalData(signal)).toMatchSignal(expected)
})

it('should redact attributes in change and in target if the listener is "mutation"', () => {
Expand All @@ -117,7 +117,7 @@ describe(redactSignalData, () => {
innerText: 'XXX',
}),
})
expect(redactSignalData(signal)).toEqual(expected)
expect(redactSignalData(signal)).toMatchSignal(expected)
})

it('should redact the textContent and innerText in the "target" property if the listener is "contenteditable"', () => {
Expand All @@ -133,7 +133,7 @@ describe(redactSignalData, () => {
change: { textContent: 'XXX' },
target: createMockTarget({ textContent: 'XXX', innerText: 'XXX' }),
})
expect(redactSignalData(signal)).toEqual(expected)
expect(redactSignalData(signal)).toMatchSignal(expected)
})

it('should redact the values in the "data" property if the type is "network"', () => {
Expand All @@ -157,7 +157,7 @@ describe(redactSignalData, () => {
},
metadataFixture
)
expect(redactSignalData(signal)).toEqual(expected)
expect(redactSignalData(signal)).toMatchSignal(expected)
})

it('should not mutate the original signal object', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ export class SignalsIngestClient {

return analytics.track(MAGIC_EVENT_NAME, {
index: this.index++,
type: signal.type,
data: cleanSignal.data,
...cleanSignal,
})
}

Expand Down
17 changes: 17 additions & 0 deletions packages/signals/signals/src/core/middleware/user-info/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Signal } from '@segment/analytics-signals-runtime'
import { UserInfo } from '../../../types'
import { SignalsMiddleware, SignalsMiddlewareContext } from '../../emitter'

export class UserInfoMiddleware implements SignalsMiddleware {
user!: UserInfo

load(ctx: SignalsMiddlewareContext) {
this.user = ctx.analyticsInstance.user()

Check warning on line 9 in packages/signals/signals/src/core/middleware/user-info/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/user-info/index.ts#L8-L9

Added lines #L8 - L9 were not covered by tests
}

process(signal: Signal): Signal {

Check warning on line 12 in packages/signals/signals/src/core/middleware/user-info/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/user-info/index.ts#L12

Added line #L12 was not covered by tests
// anonymousId should always exist here unless the user is explicitly setting it to null
signal.anonymousId = this.user.anonymousId()
return signal

Check warning on line 15 in packages/signals/signals/src/core/middleware/user-info/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/signals/signals/src/core/middleware/user-info/index.ts#L14-L15

Added lines #L14 - L15 were not covered by tests
}
}
Loading