Skip to content

Commit e0e8d0f

Browse files
authored
Be more selective about signal redaction (#1106)
1 parent bedea03 commit e0e8d0f

File tree

19 files changed

+467
-107
lines changed

19 files changed

+467
-107
lines changed

packages/signals/signals-integration-tests/src/helpers/base-page-object.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { CDNSettingsBuilder } from '@internal/test-helpers'
2-
import { Page, Request } from '@playwright/test'
2+
import { Page } from '@playwright/test'
33
import { logConsole } from './log-console'
4-
import { SegmentEvent } from '@segment/analytics-next'
54
import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals'
6-
import { PageNetworkUtils, SignalAPIRequestBuffer } from './network-utils'
5+
import {
6+
PageNetworkUtils,
7+
SignalAPIRequestBuffer,
8+
TrackingAPIRequestBuffer,
9+
} from './network-utils'
710

811
export class BasePage {
912
protected page!: Page
1013
public signalsAPI = new SignalAPIRequestBuffer()
11-
public lastTrackingApiReq!: Request
12-
public trackingApiReqs: SegmentEvent[] = []
14+
public trackingAPI = new TrackingAPIRequestBuffer()
1315
public url: string
1416
public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js'
1517
public edgeFn!: string
@@ -28,9 +30,7 @@ export class BasePage {
2830
* and wait for analytics and signals to be initialized
2931
*/
3032
async loadAndWait(...args: Parameters<BasePage['load']>) {
31-
await this.load(...args)
32-
await this.waitForSignalsAssets()
33-
return this
33+
await Promise.all([this.load(...args), this.waitForSettings()])
3434
}
3535

3636
/**
@@ -39,22 +39,24 @@ export class BasePage {
3939
async load(
4040
page: Page,
4141
edgeFn: string,
42-
signalSettings: Partial<SignalsPluginSettingsConfig> = {}
42+
signalSettings: Partial<SignalsPluginSettingsConfig> = {},
43+
options: { updateURL?: (url: string) => string } = {}
4344
) {
4445
logConsole(page)
4546
this.page = page
4647
this.network = new PageNetworkUtils(page)
4748
this.edgeFn = edgeFn
4849
await this.setupMockedRoutes()
49-
await this.page.goto(this.url)
50+
const url = options.updateURL ? options.updateURL(this.url) : this.url
51+
await this.page.goto(url)
5052
await this.invokeAnalyticsLoad(signalSettings)
5153
}
5254

5355
/**
5456
* Wait for analytics and signals to be initialized
57+
* We could do the same thing with analytics.ready() and signalsPlugin.ready()
5558
*/
56-
async waitForSignalsAssets() {
57-
// this is kind of an approximation of full initialization
59+
async waitForSettings() {
5860
return Promise.all([
5961
this.waitForCDNSettingsResponse(),
6062
this.waitForEdgeFunctionResponse(),
@@ -72,7 +74,6 @@ export class BasePage {
7274
({ signalSettings }) => {
7375
window.signalsPlugin = new window.SignalsPlugin({
7476
disableSignalsRedaction: true,
75-
flushInterval: 1000,
7677
...signalSettings,
7778
})
7879
window.analytics.load({
@@ -87,7 +88,7 @@ export class BasePage {
8788

8889
private async setupMockedRoutes() {
8990
// clear any existing saved requests
90-
this.trackingApiReqs = []
91+
this.trackingAPI.clear()
9192
this.signalsAPI.clear()
9293

9394
await Promise.all([
@@ -99,11 +100,10 @@ export class BasePage {
99100

100101
async mockTrackingApi() {
101102
await this.page.route('https://api.segment.io/v1/*', (route, request) => {
102-
this.lastTrackingApiReq = request
103-
this.trackingApiReqs.push(request.postDataJSON())
104103
if (request.method().toLowerCase() !== 'post') {
105104
throw new Error(`Unexpected method: ${request.method()}`)
106105
}
106+
this.trackingAPI.addRequest(request)
107107
return route.fulfill({
108108
contentType: 'application/json',
109109
status: 201,
@@ -122,10 +122,10 @@ export class BasePage {
122122
await this.page.route(
123123
'https://signals.segment.io/v1/*',
124124
(route, request) => {
125-
this.signalsAPI.addRequest(request)
126125
if (request.method().toLowerCase() !== 'post') {
127126
throw new Error(`Unexpected method: ${request.method()}`)
128127
}
128+
this.signalsAPI.addRequest(request)
129129
return route.fulfill({
130130
contentType: 'application/json',
131131
status: 201,
@@ -241,15 +241,17 @@ export class BasePage {
241241
})
242242
}
243243

244-
waitForEdgeFunctionResponse() {
244+
waitForEdgeFunctionResponse(timeout = 30000) {
245245
return this.page.waitForResponse(
246-
`https://cdn.edgefn.segment.com/MY-WRITEKEY/**`
246+
`https://cdn.edgefn.segment.com/MY-WRITEKEY/**`,
247+
{ timeout }
247248
)
248249
}
249250

250-
waitForCDNSettingsResponse() {
251+
async waitForCDNSettingsResponse(timeout = 30000) {
251252
return this.page.waitForResponse(
252-
'https://cdn.segment.com/v1/projects/*/settings'
253+
'https://cdn.segment.com/v1/projects/*/settings',
254+
{ timeout }
253255
)
254256
}
255257

packages/signals/signals-integration-tests/src/helpers/network-utils.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Page, Route, Request } from '@playwright/test'
22
import { SegmentEvent } from '@segment/analytics-next'
3+
import { Signal } from '@segment/analytics-signals'
34

45
type FulfillOptions = Parameters<Route['fulfill']>['0']
56
export interface XHRRequestOptions {
@@ -70,8 +71,6 @@ export class PageNetworkUtils {
7071
body: JSON.stringify({ foo: 'bar' }),
7172
...args.request,
7273
})
73-
.then(console.log)
74-
.catch(console.error)
7574
},
7675
{ url, request }
7776
)
@@ -113,18 +112,23 @@ export class PageNetworkUtils {
113112
}
114113
}
115114

116-
class SegmentAPIRequestBuffer {
115+
export class TrackingAPIRequestBuffer {
117116
private requests: Request[] = []
118-
public lastEvent() {
119-
return this.getEvents()[this.getEvents.length - 1]
117+
public lastEvent(): SegmentEvent {
118+
const allEvents = this.getEvents()
119+
return allEvents[allEvents.length - 1]
120120
}
121121
public getEvents(): SegmentEvent[] {
122-
return this.requests.flatMap((req) => req.postDataJSON().batch)
122+
return this.requests.flatMap((req) => {
123+
const body = req.postDataJSON()
124+
return 'batch' in body ? body.batch : [body]
125+
})
123126
}
124127

125128
clear() {
126129
this.requests = []
127130
}
131+
128132
addRequest(request: Request) {
129133
if (request.method().toLowerCase() !== 'post') {
130134
throw new Error(
@@ -135,18 +139,15 @@ class SegmentAPIRequestBuffer {
135139
}
136140
}
137141

138-
export class SignalAPIRequestBuffer extends SegmentAPIRequestBuffer {
139-
/**
140-
* @example 'network', 'interaction', 'navigation', etc
141-
*/
142-
override getEvents(signalType?: string): SegmentEvent[] {
142+
export class SignalAPIRequestBuffer extends TrackingAPIRequestBuffer {
143+
override getEvents(signalType?: Signal['type']): SegmentEvent[] {
143144
if (signalType) {
144145
return this.getEvents().filter((e) => e.properties!.type === signalType)
145146
}
146147
return super.getEvents()
147148
}
148149

149-
override lastEvent(signalType?: string | undefined): SegmentEvent {
150+
override lastEvent(signalType?: Signal['type']): SegmentEvent {
150151
if (signalType) {
151152
const res =
152153
this.getEvents(signalType)[this.getEvents(signalType).length - 1]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Page } from '@playwright/test'
2+
import type { Compute } from './ts'
3+
4+
export function waitForCondition(
5+
conditionFn: () => boolean,
6+
{
7+
checkInterval = 100,
8+
timeout = 10000,
9+
errorMessage = 'Condition was not met within the specified time.',
10+
} = {}
11+
): Promise<void> {
12+
return new Promise((resolve, reject) => {
13+
const startTime = Date.now()
14+
15+
const interval = setInterval(() => {
16+
try {
17+
if (conditionFn()) {
18+
clearInterval(interval)
19+
resolve()
20+
} else if (Date.now() - startTime >= timeout) {
21+
clearInterval(interval)
22+
reject(new Error(errorMessage))
23+
}
24+
} catch (error) {
25+
clearInterval(interval)
26+
reject(error)
27+
}
28+
}, checkInterval)
29+
})
30+
}
31+
32+
type FillOptions = Compute<Parameters<Page['fill']>[2]>
33+
34+
export async function fillAndBlur(
35+
page: Page,
36+
selector: string,
37+
text: string,
38+
options: FillOptions = {}
39+
) {
40+
await page.fill(selector, text, options)
41+
// Remove focus so the onChange event is triggered
42+
await page.evaluate(
43+
(args) => {
44+
const input = document.querySelector(args.selector) as HTMLElement
45+
if (input) {
46+
input.blur()
47+
}
48+
},
49+
{ selector }
50+
)
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Compute<T> = { [K in keyof T]: T[K] } & {}

packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ test('Segment events', async ({ page }) => {
5454
indexPage.waitForTrackingApiFlush(),
5555
])
5656

57-
const trackingApiReqs = indexPage.trackingApiReqs.map(normalizeSnapshotEvent)
57+
const trackingApiReqs = indexPage.trackingAPI
58+
.getEvents()
59+
.map(normalizeSnapshotEvent)
5860
expect(trackingApiReqs).toEqual(snapshot)
5961
})
6062

@@ -76,13 +78,13 @@ test('Should dispatch events from signals that occurred before analytics was ins
7678
// add a user defined signal before analytics is instantiated
7779
void indexPage.addUserDefinedSignal()
7880

79-
await indexPage.waitForSignalsAssets()
81+
await indexPage.waitForSettings()
8082

8183
await Promise.all([
8284
indexPage.waitForSignalsApiFlush(),
8385
indexPage.waitForTrackingApiFlush(),
8486
])
85-
const trackingApiReqs = indexPage.trackingApiReqs
87+
const trackingApiReqs = indexPage.trackingAPI.getEvents()
8688
expect(trackingApiReqs).toHaveLength(2)
8789

8890
const pageEvents = trackingApiReqs.find((el) => el.type === 'page')!

packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ test('network signals', async () => {
3939

4040
test('network signals xhr', async () => {
4141
/**
42-
* Make a fetch call, see if it gets sent to the signals endpoint
42+
* Make a xhr call, see if it gets sent to the signals endpoint
4343
*/
4444
await indexPage.network.mockTestRoute()
4545
await indexPage.network.makeXHRCall()
@@ -124,8 +124,7 @@ test('interaction signals', async () => {
124124
},
125125
})
126126

127-
const analyticsReqJSON = indexPage.lastTrackingApiReq.postDataJSON()
128-
127+
const analyticsReqJSON = indexPage.trackingAPI.lastEvent()
129128
expect(analyticsReqJSON).toMatchObject({
130129
writeKey: '<SOME_WRITE_KEY>',
131130
event: 'click [interaction]',

packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,33 @@ import { IndexPage } from './index-page'
33

44
const indexPage = new IndexPage()
55

6-
const basicEdgeFn = `
7-
// this is a process signal function
8-
const processSignal = (signal) => {}`
6+
const basicEdgeFn = `const processSignal = (signal) => {}`
97

108
test.beforeEach(async ({ page }) => {
119
await indexPage.loadAndWait(page, basicEdgeFn)
1210
})
1311

14-
test('button click (complex, with nested items)', async () => {
12+
const data = {
13+
eventType: 'click',
14+
target: {
15+
attributes: {
16+
id: 'complex-button',
17+
},
18+
classList: [],
19+
id: 'complex-button',
20+
labels: [],
21+
name: '',
22+
nodeName: 'BUTTON',
23+
tagName: 'BUTTON',
24+
title: '',
25+
type: 'submit',
26+
innerText: 'Other Example Button with Nested Text',
27+
textContent: 'Other Example Button with Nested Text',
28+
value: '',
29+
},
30+
}
31+
32+
test('clicking a button with nested content', async () => {
1533
/**
1634
* Click a button with nested text, ensure that that correct text shows up
1735
*/
@@ -22,28 +40,28 @@ test('button click (complex, with nested items)', async () => {
2240

2341
const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
2442
expect(interactionSignals).toHaveLength(1)
25-
const data = {
26-
eventType: 'click',
27-
target: {
28-
attributes: {
29-
id: 'complex-button',
30-
},
31-
classList: [],
32-
id: 'complex-button',
33-
labels: [],
34-
name: '',
35-
nodeName: 'BUTTON',
36-
tagName: 'BUTTON',
37-
title: '',
38-
type: 'submit',
39-
innerText: expect.any(String),
40-
textContent: expect.stringContaining(
41-
'Other Example Button with Nested Text'
42-
),
43-
value: '',
43+
44+
expect(interactionSignals[0]).toMatchObject({
45+
event: 'Segment Signal Generated',
46+
type: 'track',
47+
properties: {
48+
type: 'interaction',
49+
data,
4450
},
45-
}
51+
})
52+
})
53+
54+
test('clicking the h1 tag inside a button', async () => {
55+
/**
56+
* Click the nested text, ensure that that correct text shows up
57+
*/
58+
await Promise.all([
59+
indexPage.clickInsideComplexButton(),
60+
indexPage.waitForSignalsApiFlush(),
61+
])
4662

63+
const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
64+
expect(interactionSignals).toHaveLength(1)
4765
expect(interactionSignals[0]).toMatchObject({
4866
event: 'Segment Signal Generated',
4967
type: 'track',

0 commit comments

Comments
 (0)