Skip to content

Commit

Permalink
Be more selective about signal redaction (#1106)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Sep 30, 2024
1 parent bedea03 commit e0e8d0f
Show file tree
Hide file tree
Showing 19 changed files with 467 additions and 107 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { CDNSettingsBuilder } from '@internal/test-helpers'
import { Page, Request } from '@playwright/test'
import { Page } from '@playwright/test'
import { logConsole } from './log-console'
import { SegmentEvent } from '@segment/analytics-next'
import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals'
import { PageNetworkUtils, SignalAPIRequestBuffer } from './network-utils'
import {
PageNetworkUtils,
SignalAPIRequestBuffer,
TrackingAPIRequestBuffer,
} from './network-utils'

export class BasePage {
protected page!: Page
public signalsAPI = new SignalAPIRequestBuffer()
public lastTrackingApiReq!: Request
public trackingApiReqs: SegmentEvent[] = []
public trackingAPI = new TrackingAPIRequestBuffer()
public url: string
public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js'
public edgeFn!: string
Expand All @@ -28,9 +30,7 @@ export class BasePage {
* and wait for analytics and signals to be initialized
*/
async loadAndWait(...args: Parameters<BasePage['load']>) {
await this.load(...args)
await this.waitForSignalsAssets()
return this
await Promise.all([this.load(...args), this.waitForSettings()])
}

/**
Expand All @@ -39,22 +39,24 @@ export class BasePage {
async load(
page: Page,
edgeFn: string,
signalSettings: Partial<SignalsPluginSettingsConfig> = {}
signalSettings: Partial<SignalsPluginSettingsConfig> = {},
options: { updateURL?: (url: string) => string } = {}
) {
logConsole(page)
this.page = page
this.network = new PageNetworkUtils(page)
this.edgeFn = edgeFn
await this.setupMockedRoutes()
await this.page.goto(this.url)
const url = options.updateURL ? options.updateURL(this.url) : this.url
await this.page.goto(url)
await this.invokeAnalyticsLoad(signalSettings)
}

/**
* Wait for analytics and signals to be initialized
* We could do the same thing with analytics.ready() and signalsPlugin.ready()
*/
async waitForSignalsAssets() {
// this is kind of an approximation of full initialization
async waitForSettings() {
return Promise.all([
this.waitForCDNSettingsResponse(),
this.waitForEdgeFunctionResponse(),
Expand All @@ -72,7 +74,6 @@ export class BasePage {
({ signalSettings }) => {
window.signalsPlugin = new window.SignalsPlugin({
disableSignalsRedaction: true,
flushInterval: 1000,
...signalSettings,
})
window.analytics.load({
Expand All @@ -87,7 +88,7 @@ export class BasePage {

private async setupMockedRoutes() {
// clear any existing saved requests
this.trackingApiReqs = []
this.trackingAPI.clear()
this.signalsAPI.clear()

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

async mockTrackingApi() {
await this.page.route('https://api.segment.io/v1/*', (route, request) => {
this.lastTrackingApiReq = request
this.trackingApiReqs.push(request.postDataJSON())
if (request.method().toLowerCase() !== 'post') {
throw new Error(`Unexpected method: ${request.method()}`)
}
this.trackingAPI.addRequest(request)
return route.fulfill({
contentType: 'application/json',
status: 201,
Expand All @@ -122,10 +122,10 @@ export class BasePage {
await this.page.route(
'https://signals.segment.io/v1/*',
(route, request) => {
this.signalsAPI.addRequest(request)
if (request.method().toLowerCase() !== 'post') {
throw new Error(`Unexpected method: ${request.method()}`)
}
this.signalsAPI.addRequest(request)
return route.fulfill({
contentType: 'application/json',
status: 201,
Expand Down Expand Up @@ -241,15 +241,17 @@ export class BasePage {
})
}

waitForEdgeFunctionResponse() {
waitForEdgeFunctionResponse(timeout = 30000) {
return this.page.waitForResponse(
`https://cdn.edgefn.segment.com/MY-WRITEKEY/**`
`https://cdn.edgefn.segment.com/MY-WRITEKEY/**`,
{ timeout }
)
}

waitForCDNSettingsResponse() {
async waitForCDNSettingsResponse(timeout = 30000) {
return this.page.waitForResponse(
'https://cdn.segment.com/v1/projects/*/settings'
'https://cdn.segment.com/v1/projects/*/settings',
{ timeout }
)
}

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

type FulfillOptions = Parameters<Route['fulfill']>['0']
export interface XHRRequestOptions {
Expand Down Expand Up @@ -70,8 +71,6 @@ export class PageNetworkUtils {
body: JSON.stringify({ foo: 'bar' }),
...args.request,
})
.then(console.log)
.catch(console.error)
},
{ url, request }
)
Expand Down Expand Up @@ -113,18 +112,23 @@ export class PageNetworkUtils {
}
}

class SegmentAPIRequestBuffer {
export class TrackingAPIRequestBuffer {
private requests: Request[] = []
public lastEvent() {
return this.getEvents()[this.getEvents.length - 1]
public lastEvent(): SegmentEvent {
const allEvents = this.getEvents()
return allEvents[allEvents.length - 1]
}
public getEvents(): SegmentEvent[] {
return this.requests.flatMap((req) => req.postDataJSON().batch)
return this.requests.flatMap((req) => {
const body = req.postDataJSON()
return 'batch' in body ? body.batch : [body]
})
}

clear() {
this.requests = []
}

addRequest(request: Request) {
if (request.method().toLowerCase() !== 'post') {
throw new Error(
Expand All @@ -135,18 +139,15 @@ class SegmentAPIRequestBuffer {
}
}

export class SignalAPIRequestBuffer extends SegmentAPIRequestBuffer {
/**
* @example 'network', 'interaction', 'navigation', etc
*/
override getEvents(signalType?: string): SegmentEvent[] {
export class SignalAPIRequestBuffer extends TrackingAPIRequestBuffer {
override getEvents(signalType?: Signal['type']): SegmentEvent[] {
if (signalType) {
return this.getEvents().filter((e) => e.properties!.type === signalType)
}
return super.getEvents()
}

override lastEvent(signalType?: string | undefined): SegmentEvent {
override lastEvent(signalType?: Signal['type']): SegmentEvent {
if (signalType) {
const res =
this.getEvents(signalType)[this.getEvents(signalType).length - 1]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Page } from '@playwright/test'
import type { Compute } from './ts'

export function waitForCondition(
conditionFn: () => boolean,
{
checkInterval = 100,
timeout = 10000,
errorMessage = 'Condition was not met within the specified time.',
} = {}
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now()

const interval = setInterval(() => {
try {
if (conditionFn()) {
clearInterval(interval)
resolve()
} else if (Date.now() - startTime >= timeout) {
clearInterval(interval)
reject(new Error(errorMessage))
}
} catch (error) {
clearInterval(interval)
reject(error)
}
}, checkInterval)
})
}

type FillOptions = Compute<Parameters<Page['fill']>[2]>

export async function fillAndBlur(
page: Page,
selector: string,
text: string,
options: FillOptions = {}
) {
await page.fill(selector, text, options)
// Remove focus so the onChange event is triggered
await page.evaluate(
(args) => {
const input = document.querySelector(args.selector) as HTMLElement
if (input) {
input.blur()
}
},
{ selector }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Compute<T> = { [K in keyof T]: T[K] } & {}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ test('Segment events', async ({ page }) => {
indexPage.waitForTrackingApiFlush(),
])

const trackingApiReqs = indexPage.trackingApiReqs.map(normalizeSnapshotEvent)
const trackingApiReqs = indexPage.trackingAPI
.getEvents()
.map(normalizeSnapshotEvent)
expect(trackingApiReqs).toEqual(snapshot)
})

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

await indexPage.waitForSignalsAssets()
await indexPage.waitForSettings()

await Promise.all([
indexPage.waitForSignalsApiFlush(),
indexPage.waitForTrackingApiFlush(),
])
const trackingApiReqs = indexPage.trackingApiReqs
const trackingApiReqs = indexPage.trackingAPI.getEvents()
expect(trackingApiReqs).toHaveLength(2)

const pageEvents = trackingApiReqs.find((el) => el.type === 'page')!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test('network signals', async () => {

test('network signals xhr', async () => {
/**
* Make a fetch call, see if it gets sent to the signals endpoint
* Make a xhr call, see if it gets sent to the signals endpoint
*/
await indexPage.network.mockTestRoute()
await indexPage.network.makeXHRCall()
Expand Down Expand Up @@ -124,8 +124,7 @@ test('interaction signals', async () => {
},
})

const analyticsReqJSON = indexPage.lastTrackingApiReq.postDataJSON()

const analyticsReqJSON = indexPage.trackingAPI.lastEvent()
expect(analyticsReqJSON).toMatchObject({
writeKey: '<SOME_WRITE_KEY>',
event: 'click [interaction]',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,33 @@ import { IndexPage } from './index-page'

const indexPage = new IndexPage()

const basicEdgeFn = `
// this is a process signal function
const processSignal = (signal) => {}`
const basicEdgeFn = `const processSignal = (signal) => {}`

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

test('button click (complex, with nested items)', async () => {
const data = {
eventType: 'click',
target: {
attributes: {
id: 'complex-button',
},
classList: [],
id: 'complex-button',
labels: [],
name: '',
nodeName: 'BUTTON',
tagName: 'BUTTON',
title: '',
type: 'submit',
innerText: 'Other Example Button with Nested Text',
textContent: 'Other Example Button with Nested Text',
value: '',
},
}

test('clicking a button with nested content', async () => {
/**
* Click a button with nested text, ensure that that correct text shows up
*/
Expand All @@ -22,28 +40,28 @@ test('button click (complex, with nested items)', async () => {

const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
expect(interactionSignals).toHaveLength(1)
const data = {
eventType: 'click',
target: {
attributes: {
id: 'complex-button',
},
classList: [],
id: 'complex-button',
labels: [],
name: '',
nodeName: 'BUTTON',
tagName: 'BUTTON',
title: '',
type: 'submit',
innerText: expect.any(String),
textContent: expect.stringContaining(
'Other Example Button with Nested Text'
),
value: '',

expect(interactionSignals[0]).toMatchObject({
event: 'Segment Signal Generated',
type: 'track',
properties: {
type: 'interaction',
data,
},
}
})
})

test('clicking the h1 tag inside a button', async () => {
/**
* Click the nested text, ensure that that correct text shows up
*/
await Promise.all([
indexPage.clickInsideComplexButton(),
indexPage.waitForSignalsApiFlush(),
])

const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
expect(interactionSignals).toHaveLength(1)
expect(interactionSignals[0]).toMatchObject({
event: 'Segment Signal Generated',
type: 'track',
Expand Down
Loading

0 comments on commit e0e8d0f

Please sign in to comment.