Skip to content

Commit

Permalink
feat: type safe posthog extensions (#1407)
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra authored Sep 20, 2024
1 parent ff9d080 commit aaded54
Show file tree
Hide file tree
Showing 15 changed files with 132 additions and 56 deletions.
1 change: 1 addition & 0 deletions .github/workflows/label-alpha-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: https://registry.npmjs.org
cache: 'pnpm'
- run: pnpm install

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Exception Observer', () => {
// assignableWindow.onerror = jest.fn()
// assignableWindow.onerror__POSTHOG_INSTRUMENTED__ = true

assignableWindow.posthogErrorHandlers = posthogErrorWrappingFunctions
assignableWindow.__PosthogExtensions__.errorWrappingFunctions = posthogErrorWrappingFunctions
}

beforeEach(async () => {
Expand Down
35 changes: 22 additions & 13 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,21 @@ describe('SessionRecording', () => {
let addCaptureHookMock: Mock

const addRRwebToWindow = () => {
assignableWindow.rrweb = {
assignableWindow.__PosthogExtensions__.rrweb = {
record: jest.fn(({ emit }) => {
_emit = emit
return () => {}
}),
version: 'fake',
rrwebVersion: 'fake',
}
assignableWindow.rrweb.record.takeFullSnapshot = jest.fn(() => {
assignableWindow.__PosthogExtensions__.rrweb.record.takeFullSnapshot = jest.fn(() => {
// we pretend to be rrweb and call emit
_emit(createFullSnapshot())
})
assignableWindow.rrweb.record.addCustomEvent = _addCustomEvent
assignableWindow.__PosthogExtensions__.rrweb.record.addCustomEvent = _addCustomEvent

assignableWindow.rrwebConsoleRecord = {
assignableWindow.__PosthogExtensions__.rrwebPlugins = {
getRecordConsolePlugin: jest.fn(),
}
}
Expand All @@ -165,8 +167,13 @@ describe('SessionRecording', () => {
persistence: 'memory',
} as unknown as PostHogConfig

assignableWindow.rrweb = undefined
assignableWindow.rrwebConsoleRecord = undefined
assignableWindow.__PosthogExtensions__ = {
rrweb: undefined,
rrwebPlugins: {
getRecordConsolePlugin: undefined,
getRecordNetworkPlugin: undefined,
},
}

sessionIdGeneratorMock = jest.fn().mockImplementation(() => sessionId)
windowIdGeneratorMock = jest.fn().mockImplementation(() => 'windowId')
Expand Down Expand Up @@ -636,7 +643,7 @@ describe('SessionRecording', () => {
sessionRecording.startIfEnabledOrStop()

sessionRecording['_onScriptLoaded']()
expect(assignableWindow.rrweb.record).toHaveBeenCalledWith(
expect(assignableWindow.__PosthogExtensions__.rrweb.record).toHaveBeenCalledWith(
expect.objectContaining({
recordCanvas: true,
sampling: { canvas: 6 },
Expand All @@ -659,7 +666,7 @@ describe('SessionRecording', () => {

sessionRecording['_onScriptLoaded']()

const mockParams = assignableWindow.rrweb.record.mock.calls[0][0]
const mockParams = assignableWindow.__PosthogExtensions__.rrweb.record.mock.calls[0][0]
expect(mockParams).not.toHaveProperty('recordCanvas')
expect(mockParams).not.toHaveProperty('canvasFps')
expect(mockParams).not.toHaveProperty('canvasQuality')
Expand All @@ -672,7 +679,7 @@ describe('SessionRecording', () => {
sessionRecording.startIfEnabledOrStop()
// maskAllInputs should change from default
// someUnregisteredProp should not be present
expect(assignableWindow.rrweb.record).toHaveBeenCalledWith({
expect(assignableWindow.__PosthogExtensions__.rrweb.record).toHaveBeenCalledWith({
emit: expect.anything(),
maskAllInputs: false,
blockClass: 'ph-no-capture',
Expand Down Expand Up @@ -700,7 +707,7 @@ describe('SessionRecording', () => {
])('%s', (_name: string, session_recording: SessionRecordingOptions, expected: boolean) => {
posthog.config.session_recording = session_recording
sessionRecording.startIfEnabledOrStop()
expect(assignableWindow.rrweb.record).toHaveBeenCalledWith(
expect(assignableWindow.__PosthogExtensions__.rrweb.record).toHaveBeenCalledWith(
expect.objectContaining({
maskInputOptions: expect.objectContaining({ password: expected }),
})
Expand Down Expand Up @@ -999,15 +1006,17 @@ describe('SessionRecording', () => {

sessionRecording.startIfEnabledOrStop()

expect(assignableWindow.rrwebConsoleRecord.getRecordConsolePlugin).not.toHaveBeenCalled()
expect(
assignableWindow.__PosthogExtensions__.rrwebPlugins.getRecordConsolePlugin
).not.toHaveBeenCalled()
})

it('if enabled, plugin is used', () => {
posthog.config.enable_recording_console_log = true

sessionRecording.startIfEnabledOrStop()

expect(assignableWindow.rrwebConsoleRecord.getRecordConsolePlugin).toHaveBeenCalled()
expect(assignableWindow.__PosthogExtensions__.rrwebPlugins.getRecordConsolePlugin).toHaveBeenCalled()
})
})

Expand Down Expand Up @@ -1246,7 +1255,7 @@ describe('SessionRecording', () => {
startingTimestamp = sessionRecording['_lastActivityTimestamp']
expect(startingTimestamp).toBeGreaterThan(0)

expect(assignableWindow.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(0)
expect(assignableWindow.__PosthogExtensions__.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(0)

// the buffer starts out empty
expect(sessionRecording['buffer']).toEqual({
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/surveys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('surveys', () => {

loadScriptMock.mockImplementation((_path, callback) => {
assignableWindow.__PosthogExtensions__ = assignableWindow.__Posthog__ || {}
assignableWindow.extendPostHogWithSurveys = generateSurveys
assignableWindow.__PosthogExtensions__.generateSurveys = generateSurveys
assignableWindow.__PosthogExtensions__.canActivateRepeatedly = canActivateRepeatedly

callback()
Expand Down
17 changes: 12 additions & 5 deletions src/entrypoints/exception-autocapture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { errorToProperties, unhandledRejectionToProperties } from '../extensions/exception-autocapture/error-conversion'
import { window } from '../utils/globals'
import { assignableWindow, window } from '../utils/globals'
import { ErrorEventArgs, Properties } from '../types'
import { logger } from '../utils/logger'

Expand Down Expand Up @@ -49,9 +49,16 @@ const posthogErrorWrappingFunctions = {
wrapUnhandledRejection,
}

if (window) {
;(window as any).posthogErrorWrappingFunctions = posthogErrorWrappingFunctions
;(window as any).parseErrorAsProperties = errorToProperties
}
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
assignableWindow.__PosthogExtensions__.errorWrappingFunctions = posthogErrorWrappingFunctions
assignableWindow.__PosthogExtensions__.parseErrorAsProperties = errorToProperties

// we used to put these on window, and now we put them on __PosthogExtensions__
// but that means that old clients which lazily load this extension are looking in the wrong place
// yuck,
// so we also put them directly on the window
// when 1.161.1 is the oldest version seen in production we can remove this
assignableWindow.posthogErrorWrappingFunctions = posthogErrorWrappingFunctions
assignableWindow.parseErrorAsProperties = errorToProperties

export default posthogErrorWrappingFunctions
19 changes: 13 additions & 6 deletions src/entrypoints/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
isUndefined,
} from '../utils/type-utils'
import { logger } from '../utils/logger'
import { window } from '../utils/globals'
import { assignableWindow } from '../utils/globals'
import { defaultNetworkOptions } from '../extensions/replay/config'
import { formDataToQuery } from '../utils/request-utils'
import { patch } from '../extensions/replay/rrweb-plugins/patch'
Expand Down Expand Up @@ -674,10 +674,17 @@ export const getRecordNetworkPlugin: (options?: NetworkRecordOptions) => RecordP

// rrweb/networ@1 ends

if (window) {
;(window as any).rrweb = { record: rrwebRecord, version: 'v2', rrwebVersion: version }
;(window as any).rrwebConsoleRecord = { getRecordConsolePlugin }
;(window as any).getRecordNetworkPlugin = getRecordNetworkPlugin
}
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
assignableWindow.__PosthogExtensions__.rrwebPlugins = { getRecordConsolePlugin, getRecordNetworkPlugin }
assignableWindow.__PosthogExtensions__.rrweb = { record: rrwebRecord, version: 'v2', rrwebVersion: version }

// we used to put all of these items directly on window, and now we put it on __PosthogExtensions__
// but that means that old clients which lazily load this extension are looking in the wrong place
// yuck,
// so we also put them directly on the window
// when 1.161.1 is the oldest version seen in production we can remove this
assignableWindow.rrweb = { record: rrwebRecord, version: 'v2', rrwebVersion: version }
assignableWindow.rrwebConsoleRecord = { getRecordConsolePlugin }
assignableWindow.getRecordNetworkPlugin = getRecordNetworkPlugin

export default rrwebRecord
14 changes: 8 additions & 6 deletions src/entrypoints/surveys.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { generateSurveys } from '../extensions/surveys'

import { window } from '../utils/globals'
import { assignableWindow } from '../utils/globals'
import { canActivateRepeatedly } from '../extensions/surveys/surveys-utils'

if (window) {
;(window as any).__PosthogExtensions__ = (window as any).__PosthogExtensions__ || {}
;(window as any).__PosthogExtensions__.canActivateRepeatedly = canActivateRepeatedly
;(window as any).extendPostHogWithSurveys = generateSurveys
}
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
assignableWindow.__PosthogExtensions__.canActivateRepeatedly = canActivateRepeatedly
assignableWindow.__PosthogExtensions__.generateSurveys = generateSurveys

// this used to be directly on window, but we moved it to __PosthogExtensions__
// it is still on window for backwards compatibility
assignableWindow.extendPostHogWithSurveys = generateSurveys

export default generateSurveys
19 changes: 14 additions & 5 deletions src/entrypoints/tracing-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ const patchXHR = (sessionManager: SessionIdManager): (() => void) => {
)
}

if (assignableWindow) {
assignableWindow.postHogTracingHeadersPatchFns = {
_patchFetch: patchFetch,
_patchXHR: patchXHR,
}
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
const patchFns = {
_patchFetch: patchFetch,
_patchXHR: patchXHR,
}
assignableWindow.__PosthogExtensions__.tracingHeadersPatchFns = patchFns

// we used to put tracingHeadersPatchFns on window, and now we put it on __PosthogExtensions__
// but that means that old clients which lazily load this extension are looking in the wrong place
// yuck,
// so we also put it directly on the window
// when 1.161.1 is the oldest version seen in production we can remove this
assignableWindow.postHogTracingHeadersPatchFns = patchFns

export default patchFns
7 changes: 4 additions & 3 deletions src/extensions/exception-autocapture/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { window } from '../../utils/globals'
import { assignableWindow, window } from '../../utils/globals'
import { PostHog } from '../../posthog-core'
import { DecideResponse, Properties } from '../../types'

Expand Down Expand Up @@ -60,8 +60,9 @@ export class ExceptionObserver {
return
}

const wrapOnError = (window as any).posthogErrorWrappingFunctions.wrapOnError
const wrapUnhandledRejection = (window as any).posthogErrorWrappingFunctions.wrapUnhandledRejection
const wrapOnError = assignableWindow.__PosthogExtensions__?.errorWrappingFunctions?.wrapOnError
const wrapUnhandledRejection =
assignableWindow.__PosthogExtensions__?.errorWrappingFunctions?.wrapUnhandledRejection

if (!wrapOnError || !wrapUnhandledRejection) {
logger.error(LOGGER_PREFIX + ' failed to load error wrapping functions - cannot start')
Expand Down
14 changes: 7 additions & 7 deletions src/extensions/replay/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class SessionRecording {
_forceAllowLocalhostNetworkCapture = false

private get rrwebRecord(): rrwebRecord | undefined {
return assignableWindow?.rrweb?.record
return assignableWindow?.__PosthogExtensions__?.rrweb?.record
}

public get started(): boolean {
Expand Down Expand Up @@ -735,18 +735,18 @@ export class SessionRecording {
private _gatherRRWebPlugins() {
const plugins: RecordPlugin<unknown>[] = []

if (assignableWindow.rrwebConsoleRecord && this.isConsoleLogCaptureEnabled) {
plugins.push(assignableWindow.rrwebConsoleRecord.getRecordConsolePlugin())
const recordConsolePlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordConsolePlugin
if (recordConsolePlugin && this.isConsoleLogCaptureEnabled) {
plugins.push(recordConsolePlugin())
}

if (this.networkPayloadCapture && isFunction(assignableWindow.getRecordNetworkPlugin)) {
const networkPlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin
if (this.networkPayloadCapture && isFunction(networkPlugin)) {
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture

if (canRecordNetwork) {
plugins.push(
assignableWindow.getRecordNetworkPlugin(
buildNetworkRequestOptions(this.instance.config, this.networkPayloadCapture)
)
networkPlugin(buildNetworkRequestOptions(this.instance.config, this.networkPayloadCapture))
)
} else {
logger.info(LOGGER_PREFIX + ' NetworkCapture not started because we are on localhost.')
Expand Down
6 changes: 3 additions & 3 deletions src/extensions/tracing-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class TracingHeaders {
constructor(private readonly instance: PostHog) {}

private _loadScript(cb: () => void): void {
if (assignableWindow.postHogTracingHeadersPatchFns) {
if (assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns) {
// already loaded
cb()
}
Expand All @@ -40,10 +40,10 @@ export class TracingHeaders {
private _startCapturing = () => {
// NB: we can assert sessionManager is present only because we've checked previously
if (isUndefined(this._restoreXHRPatch)) {
assignableWindow.postHogTracingHeadersPatchFns._patchXHR(this.instance.sessionManager!)
assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchXHR(this.instance.sessionManager!)
}
if (isUndefined(this._restoreFetchPatch)) {
assignableWindow.postHogTracingHeadersPatchFns._patchFetch(this.instance.sessionManager!)
assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchFetch(this.instance.sessionManager!)
}
}
}
2 changes: 1 addition & 1 deletion src/extensions/web-vitals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class WebVitalsAutocapture {
let onINP: WebVitalsMetricCallback | undefined

const posthogExtensions = assignableWindow.__PosthogExtensions__
if (!isUndefined(posthogExtensions)) {
if (!isUndefined(posthogExtensions) && !isUndefined(posthogExtensions.postHogWebVitalsCallbacks)) {
;({ onLCP, onCLS, onFCP, onINP } = posthogExtensions.postHogWebVitalsCallbacks)
}

Expand Down
10 changes: 8 additions & 2 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1822,8 +1822,14 @@ export class PostHog {

/** Capture a caught exception manually */
captureException(error: Error, additionalProperties?: Properties): void {
const properties: Properties = isFunction(assignableWindow.parseErrorAsProperties)
? assignableWindow.parseErrorAsProperties([error.message, undefined, undefined, undefined, error])
const properties: Properties = isFunction(assignableWindow.__PosthogExtensions__?.parseErrorAsProperties)
? assignableWindow.__PosthogExtensions__.parseErrorAsProperties([
error.message,
undefined,
undefined,
undefined,
error,
])
: {
$exception_type: error.name,
$exception_message: error.message,
Expand Down
4 changes: 2 additions & 2 deletions src/posthog-surveys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class PostHogSurveys {
}

loadIfEnabled() {
const surveysGenerator = assignableWindow?.extendPostHogWithSurveys
const surveysGenerator = assignableWindow?.__PosthogExtensions__?.generateSurveys

if (!this.instance.config.disable_surveys && this._decideServerResponse && !surveysGenerator) {
if (this._surveyEventReceiver == null) {
Expand All @@ -85,7 +85,7 @@ export class PostHogSurveys {
return logger.error(LOGGER_PREFIX, 'Could not load surveys script', err)
}

this._surveyManager = assignableWindow.extendPostHogWithSurveys(this.instance)
this._surveyManager = assignableWindow.__PosthogExtensions__?.generateSurveys?.(this.instance)
})
}
}
Expand Down
Loading

0 comments on commit aaded54

Please sign in to comment.