Skip to content

Commit

Permalink
feat: Allow custom masking of network events (#620)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite committed May 9, 2023
1 parent 64f463c commit 81d2f45
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^8.3.3",
"@sentry/types": "7.37.2",
"@types/jest": "^29.5.1",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
Expand Down
135 changes: 135 additions & 0 deletions src/__tests__/extensions/web-performance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { WebPerformanceObserver } from '../../extensions/web-performance'
import { PostHog } from '../../posthog-core'
import { NetworkRequest, PostHogConfig } from '../../types'

const createMockPerformanceEntry = (overrides: Partial<PerformanceEntry> = {}): PerformanceEntry => {
const entry = {
name: 'http://example.com/api/1',
duration: 100,
entryType: 'fetch',
startTime: Date.now() - 1000,
...overrides,
toJSON: () => {
return {
...entry,
toJSON: undefined,
}
},
}

return entry
}

describe('WebPerformance', () => {
let webPerformance: WebPerformanceObserver
let mockPostHogInstance: any
const mockConfig: Partial<PostHogConfig> = {
api_host: 'https://app.posthog.com',
session_recording: {
maskNetworkRequestFn: (networkRequest: NetworkRequest) => networkRequest,
},
}

beforeEach(() => {
mockPostHogInstance = {
get_config: jest.fn((key: string) => mockConfig[key]),
sessionRecording: {
onRRwebEmit: jest.fn(),
},
}
webPerformance = new WebPerformanceObserver(mockPostHogInstance as PostHog)
jest.clearAllMocks()
jest.useFakeTimers()
jest.setSystemTime(new Date('2023-01-01'))
performance.now = jest.fn(() => Date.now())
})

describe('_capturePerformanceEvent', () => {
it('should capture and save a standard perf event', () => {
webPerformance._capturePerformanceEvent(
createMockPerformanceEntry({
name: 'http://example.com/api/1',
})
)

expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledTimes(1)
expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledWith({
data: {
payload: {
'0': 'fetch',
'1': 0,
'2': 'http://example.com/api/1',
'3': 1672531199000,
'39': 100,
'40': 1672531199000,
},
plugin: 'posthog/network@1',
},
timestamp: 1672531199000,
type: 6,
})
})

it('should ignore posthog network events', () => {
webPerformance._capturePerformanceEvent(
createMockPerformanceEntry({
name: 'https://app.posthog.com/s/',
})
)

expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledTimes(0)
})

it('should ignore events with maskNetworkRequestFn returning null', () => {
mockConfig.session_recording!.maskNetworkRequestFn = (event) => {
if (event.url.includes('ignore')) {
return null
}
return event
}
;[
'https://example.com/ignore/',
'https://example.com/capture/',
'https://ignore.example.com/capture/',
].forEach((url) => {
webPerformance._capturePerformanceEvent(
createMockPerformanceEntry({
name: url,
})
)
})
expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledTimes(1)
})

it('should allow modifying of the content via maskNetworkRequestFn', () => {
mockConfig.session_recording!.maskNetworkRequestFn = (event) => {
event.url = event.url.replace('example', 'replaced')
return event
}

webPerformance._capturePerformanceEvent(
createMockPerformanceEntry({
name: 'https://example.com/capture/',
})
)

expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledTimes(1)
expect(mockPostHogInstance.sessionRecording.onRRwebEmit).toHaveBeenCalledWith({
data: {
payload: {
'0': 'fetch',
'1': 0,
'2': 'https://replaced.com/capture/',
'3': 1672531199000,
'39': 100,
'40': 1672531199000,
},
plugin: 'posthog/network@1',
},
timestamp: 1672531199000,
type: 6,
})
})
})
})
19 changes: 18 additions & 1 deletion src/extensions/web-performance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isLocalhost, logger } from '../utils'
import { PostHog } from '../posthog-core'
import { DecideResponse } from '../types'
import { DecideResponse, NetworkRequest } from '../types'

const PERFORMANCE_EVENTS_MAPPING: { [key: string]: number } = {
// BASE_PERFORMANCE_EVENT_COLUMNS
Expand Down Expand Up @@ -157,7 +157,24 @@ export class WebPerformanceObserver {
}
}

// NOTE: This is minimal atm but will include more options when we move to the
// built in rrweb network recorder
let networkRequest: NetworkRequest | null | undefined = {
url: event.name,
}

const userSessionRecordingOptions = this.instance.get_config('session_recording')

if (userSessionRecordingOptions.maskNetworkRequestFn) {
networkRequest = userSessionRecordingOptions.maskNetworkRequestFn(networkRequest)
}

if (!networkRequest) {
return
}

const eventJson = event.toJSON()
eventJson.name = networkRequest.url
const properties: { [key: number]: any } = {}
// kudos to sentry javascript sdk for excellent background on why to use Date.now() here
// https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export interface SessionRecordingOptions {
maskAllInputs?: boolean
maskInputOptions?: MaskInputOptions
maskInputFn?: ((text: string, element?: HTMLElement) => string) | null
/** Modify the network request before it is captured. Returning null stops it being captured */
maskNetworkRequestFn?: ((url: NetworkRequest) => NetworkRequest | null | undefined) | null
slimDOMOptions?: SlimDOMOptions | 'all' | true
collectFonts?: boolean
inlineStylesheet?: boolean
Expand Down Expand Up @@ -311,3 +313,7 @@ export type EarlyAccessFeatureCallback = (earlyAccessFeatures: EarlyAccessFeatur
export interface EarlyAccessFeatureResponse {
earlyAccessFeatures: EarlyAccessFeature[]
}

export type NetworkRequest = {
url: string
}
Loading

0 comments on commit 81d2f45

Please sign in to comment.