Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signals analytics runtime #1168

Merged
merged 4 commits into from
Oct 30, 2024
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
4 changes: 4 additions & 0 deletions .changeset/proud-meals-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
'@segment/analytics-signals-runtime': major
---
Release analytics-signals-runtime
4 changes: 4 additions & 0 deletions .changeset/proud-meals-ox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
'@segment/analytics-signals': minor
---
Refactor runtime to use `@segment/analytics-signals-runtime`
8 changes: 7 additions & 1 deletion meta-tests/check-dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import fs from 'fs'
*/
const execa = promisify(exec)

const allPublicPackageDirNames = ['browser', 'core', 'node'] as const
const allPublicPackageDirNames = [
'browser',
'core',
'node',
'signals/signals',
'signals/signals-runtime',
] as const

type PackageDirName = typeof allPublicPackageDirNames[number]

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"devDependencies": {
"@changesets/changelog-github": "^0.4.5",
"@changesets/cli": "^2.23.2",
"@microsoft/api-extractor": "^7.47.9",
"@npmcli/promise-spawn": "^7.0.0",
"@types/express": "4",
"@types/jest": "^29.5.11",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class BasePage {
*/
async loadAndWait(...args: Parameters<BasePage['load']>) {
await Promise.all([this.load(...args), this.waitForSettings()])
return this
}

/**
Expand All @@ -48,8 +49,12 @@ export class BasePage {
this.edgeFn = edgeFn
await this.setupMockedRoutes()
const url = options.updateURL ? options.updateURL(this.url) : this.url
await this.page.goto(url)
void this.invokeAnalyticsLoad(signalSettings)
await this.page.goto(url, { waitUntil: 'domcontentloaded' })
void this.invokeAnalyticsLoad({
flushInterval: 500,
...signalSettings,
})
return this
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Page, Route, Request } from '@playwright/test'
import { SegmentEvent } from '@segment/analytics-next'
import { Signal } from '@segment/analytics-signals'
import { waitForCondition } from './playwright-utils'

type FulfillOptions = Parameters<Route['fulfill']>['0']
export interface XHRRequestOptions {
Expand Down Expand Up @@ -160,6 +161,31 @@ export class TrackingAPIRequestBuffer {
}

export class SignalAPIRequestBuffer extends TrackingAPIRequestBuffer {
async waitForEvents(
numberOfSignals: number,
signalType?: Signal['type']
): Promise<SegmentEvent[]> {
await waitForCondition(
() => this.getEvents(signalType).length >= numberOfSignals,
{
timeout: 5000,
errorMessage: `Found ${
this.getEvents(signalType).length
} signals, expected ${numberOfSignals}`,
}
)
const events = this.getEvents(signalType)
if (events.length < numberOfSignals) {
throw new Error(
`Expected ${numberOfSignals} signals of type ${signalType}, but got ${
this.getEvents(signalType).length
}`
)
} else {
return events
}
}

override getEvents(signalType?: Signal['type']): SegmentEvent[] {
if (signalType) {
return this.getEvents().filter((e) => e.properties!.type === signalType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const indexPage = new IndexPage()

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {
Expand All @@ -12,8 +10,10 @@ const basicEdgeFn = `
}
}`

let indexPage: IndexPage

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

test('network signals fetch', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const indexPage = new IndexPage()

const basicEdgeFn = `const processSignal = (signal) => {}`

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

const data = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ 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 ({
Expand All @@ -12,7 +10,7 @@ test('Collecting signals whenever a user enters text input', async ({
/**
* Input some text into the input field, see if the signal is emitted correctly
*/
await indexPage.loadAndWait(page, basicEdgeFn, {
const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, {
disableSignalsRedaction: true,
enableSignalsIngestion: true,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const indexPage = new IndexPage()

const basicEdgeFn = `const processSignal = (signal) => {}`

test('network signals allow and disallow list', async ({ page }) => {
await indexPage.loadAndWait(page, basicEdgeFn, {
const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, {
networkSignalsAllowList: ['allowed-api.com'],
networkSignalsDisallowList: ['https://disallowed-api.com/api/foo'],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ test.describe('network signals - fetch', () => {
let indexPage: IndexPage

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

test('should not emit anything if neither request nor response are json', async () => {
Expand Down Expand Up @@ -55,20 +54,23 @@ test.describe('network signals - fetch', () => {
(el) => el.properties!.data.action === 'request'
)
expect(requests).toHaveLength(1)
expect(requests[0].properties!.data).toMatchObject({
expect(requests[0].properties!.data).toEqual({
action: 'request',
url: 'http://localhost/test',
method: 'POST',
data: { key: 'value' },
})

const responses = networkEvents.filter(
(el) => el.properties!.data.action === 'response'
)
expect(responses).toHaveLength(1)
expect(responses[0].properties!.data).toMatchObject({
expect(responses[0].properties!.data).toEqual({
action: 'response',
url: 'http://localhost/test',
data: { foo: 'test' },
status: 200,
ok: true,
})
})

Expand Down Expand Up @@ -177,6 +179,8 @@ test.describe('network signals - fetch', () => {
action: 'response',
url: 'http://localhost/test',
data: { errorMsg: 'foo' },
status: 400,
ok: false,
})
expect(responses).toHaveLength(1)
})
Expand Down Expand Up @@ -214,6 +218,8 @@ test.describe('network signals - fetch', () => {
action: 'response',
url: 'http://localhost/test',
data: 'foo',
status: 400,
ok: false,
})
expect(responses).toHaveLength(1)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'
import { sleep } from '@segment/analytics-core'

const basicEdgeFn = `const processSignal = (signal) => {}`

test.describe('network signals - XHR', () => {
let indexPage: IndexPage

test.beforeEach(async ({ page }) => {
indexPage = new IndexPage()
await indexPage.loadAndWait(page, basicEdgeFn)
indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
})
test('should not emit anything if neither request nor response are json', async () => {
await indexPage.network.mockTestRoute('http://localhost/test', {
Expand All @@ -24,7 +24,7 @@ test.describe('network signals - XHR', () => {
})

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()
await sleep(300)

const networkEvents = indexPage.signalsAPI.getEvents('network')

Expand All @@ -46,10 +46,7 @@ test.describe('network signals - XHR', () => {

expect(data).toEqual({ foo: 'test' })

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()

const networkEvents = indexPage.signalsAPI.getEvents('network')
const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network')

// Check the request
const requests = networkEvents.filter(
Expand Down Expand Up @@ -88,9 +85,7 @@ test.describe('network signals - XHR', () => {
})

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()

const networkEvents = indexPage.signalsAPI.getEvents('network')
const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network')

// Check the request
const requests = networkEvents.filter(
Expand Down Expand Up @@ -130,9 +125,8 @@ test.describe('network signals - XHR', () => {
})

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()

const networkEvents = indexPage.signalsAPI.getEvents('network')
const networkEvents = await indexPage.signalsAPI.waitForEvents(1, 'network')

// Check the response (only response should be captured)
const responses = networkEvents.filter(
Expand Down Expand Up @@ -163,7 +157,7 @@ test.describe('network signals - XHR', () => {
})

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()
await indexPage.signalsAPI.waitForEvents(1, 'network')

// Retrieve the batch of events from the signals request
const networkEvents = indexPage.signalsAPI.getEvents('network')
Expand Down Expand Up @@ -211,10 +205,8 @@ test.describe('network signals - XHR', () => {
])

// Wait for the signals to be flushed
await indexPage.waitForSignalsApiFlush()
const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network')

// Retrieve the batch of events from the signals request
const networkEvents = indexPage.signalsAPI.getEvents('network')
// Check the request
const requests = networkEvents.filter(
(el) => el.properties!.data.action === 'request'
Expand Down Expand Up @@ -252,7 +244,7 @@ test.describe('network signals - XHR', () => {
contentType: 'application/json',
})

await indexPage.waitForSignalsApiFlush()
await indexPage.signalsAPI.waitForEvents(2, 'network')

const networkEvents = indexPage.signalsAPI.getEvents('network')

Expand Down Expand Up @@ -290,7 +282,7 @@ test.describe('network signals - XHR', () => {
contentType: 'application/json',
})

await indexPage.waitForSignalsApiFlush()
await indexPage.signalsAPI.waitForEvents(2, 'network')

const networkEvents = indexPage.signalsAPI.getEvents('network')

Expand Down Expand Up @@ -331,8 +323,7 @@ test.describe('network signals - XHR', () => {
responseType: 'json',
contentType: 'application/json',
})

await indexPage.waitForSignalsApiFlush()
await indexPage.signalsAPI.waitForEvents(2, 'network')

const networkEvents = indexPage.signalsAPI.getEvents('network')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const basicEdgeFn = `
const processSignal = (signal) => {
// test that constants are properly injected
if (typeof EventType !== 'object') {
throw new Error('EventType is missing?')
}
if (typeof SignalType !== 'object') {
throw new Error('SignalType is missing?')
}
if (typeof NavigationAction !== 'object') {
throw new Error('NavigationAction is missing?')
}

if (signal.type === SignalType.Interaction) {
const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']'
analytics.track(eventName, signal.data)
}
}`

let indexPage: IndexPage

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

test('constants should be accessible in the runtime', async () => {
/**
* Make a button click, see ifdom.window.NavigationAction.URLChange it:
* - creates an interaction signal that sends to the signals endpoint
* - creates an analytics event that sends to the tracking endpoint
*/
await Promise.all([
indexPage.clickButton(),
indexPage.waitForTrackingApiFlush(),
])

const analyticsReqJSON = indexPage.trackingAPI.lastEvent()
expect(analyticsReqJSON).toMatchObject({
event: 'click [interaction]',
})
})
Loading
Loading