diff --git a/.github/workflows/label-alpha-release.yml b/.github/workflows/label-alpha-release.yml index b3d34682d..ab149f050 100644 --- a/.github/workflows/label-alpha-release.yml +++ b/.github/workflows/label-alpha-release.yml @@ -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 diff --git a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts index cffc71572..2070a2c41 100644 --- a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts +++ b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts @@ -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 () => { diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index f300af56d..fd26a40c6 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -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(), } } @@ -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') @@ -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 }, @@ -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') @@ -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', @@ -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 }), }) @@ -999,7 +1006,9 @@ describe('SessionRecording', () => { sessionRecording.startIfEnabledOrStop() - expect(assignableWindow.rrwebConsoleRecord.getRecordConsolePlugin).not.toHaveBeenCalled() + expect( + assignableWindow.__PosthogExtensions__.rrwebPlugins.getRecordConsolePlugin + ).not.toHaveBeenCalled() }) it('if enabled, plugin is used', () => { @@ -1007,7 +1016,7 @@ describe('SessionRecording', () => { sessionRecording.startIfEnabledOrStop() - expect(assignableWindow.rrwebConsoleRecord.getRecordConsolePlugin).toHaveBeenCalled() + expect(assignableWindow.__PosthogExtensions__.rrwebPlugins.getRecordConsolePlugin).toHaveBeenCalled() }) }) @@ -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({ diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index aed07e393..0074a3bf0 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -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() diff --git a/src/entrypoints/exception-autocapture.ts b/src/entrypoints/exception-autocapture.ts index f1411b62d..07dd05bad 100644 --- a/src/entrypoints/exception-autocapture.ts +++ b/src/entrypoints/exception-autocapture.ts @@ -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' @@ -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 diff --git a/src/entrypoints/recorder.ts b/src/entrypoints/recorder.ts index a2f41db5c..fe0f4a9af 100644 --- a/src/entrypoints/recorder.ts +++ b/src/entrypoints/recorder.ts @@ -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' @@ -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 diff --git a/src/entrypoints/surveys.ts b/src/entrypoints/surveys.ts index 12606dffb..4baba2ca7 100644 --- a/src/entrypoints/surveys.ts +++ b/src/entrypoints/surveys.ts @@ -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 diff --git a/src/entrypoints/tracing-headers.ts b/src/entrypoints/tracing-headers.ts index 2d76e52f1..06abc4e01 100644 --- a/src/entrypoints/tracing-headers.ts +++ b/src/entrypoints/tracing-headers.ts @@ -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 diff --git a/src/extensions/exception-autocapture/index.ts b/src/extensions/exception-autocapture/index.ts index d820812b5..be1996015 100644 --- a/src/extensions/exception-autocapture/index.ts +++ b/src/extensions/exception-autocapture/index.ts @@ -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' @@ -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') diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index c4b592556..aaa94d439 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -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 { @@ -735,18 +735,18 @@ export class SessionRecording { private _gatherRRWebPlugins() { const plugins: RecordPlugin[] = [] - 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.') diff --git a/src/extensions/tracing-headers.ts b/src/extensions/tracing-headers.ts index 02c9a05fd..6ed536049 100644 --- a/src/extensions/tracing-headers.ts +++ b/src/extensions/tracing-headers.ts @@ -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() } @@ -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!) } } } diff --git a/src/extensions/web-vitals/index.ts b/src/extensions/web-vitals/index.ts index 622fde5e0..ea5987e38 100644 --- a/src/extensions/web-vitals/index.ts +++ b/src/extensions/web-vitals/index.ts @@ -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) } diff --git a/src/posthog-core.ts b/src/posthog-core.ts index aa9dbba86..3dee01b08 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -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, diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index 331d76643..d7a9f0320 100644 --- a/src/posthog-surveys.ts +++ b/src/posthog-surveys.ts @@ -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) { @@ -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) }) } } diff --git a/src/utils/globals.ts b/src/utils/globals.ts index 48472df92..294866c1a 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -1,3 +1,7 @@ +import { SessionIdManager } from '../sessionid' +import { ErrorEventArgs, ErrorProperties, Properties } from '../types' +import { PostHog } from '../posthog-core' + /* * Global helpers to protect access to browser globals in a way that is safer for different targets * like DOM, SSR, Web workers etc. @@ -11,6 +15,32 @@ // eslint-disable-next-line no-restricted-globals const win: (Window & typeof globalThis) | undefined = typeof window !== 'undefined' ? window : undefined +/** + * This is our contract between (potentially) lazily loaded extensions and the SDK + * changes to this interface can be breaking changes for users of the SDK + */ +interface PosthogExtensions { + parseErrorAsProperties?: ([event, source, lineno, colno, error]: ErrorEventArgs) => ErrorProperties + errorWrappingFunctions?: { + wrapOnError: (captureFn: (props: Properties) => void) => () => void + wrapUnhandledRejection: (captureFn: (props: Properties) => void) => () => void + } + rrweb?: { record: any; version: string; rrwebVersion: string } + rrwebPlugins?: { getRecordConsolePlugin: any; getRecordNetworkPlugin?: any } + canActivateRepeatedly?: (survey: any) => boolean + generateSurveys?: (posthog: PostHog) => any | undefined + postHogWebVitalsCallbacks?: { + onLCP: (metric: any) => void + onCLS: (metric: any) => void + onFCP: (metric: any) => void + onINP: (metric: any) => void + } + tracingHeadersPatchFns?: { + _patchFetch: (sessionManager: SessionIdManager) => () => void + _patchXHR: (sessionManager: any) => () => void + } +} + const global: typeof globalThis | undefined = typeof globalThis !== 'undefined' ? globalThis : win export const ArrayProto = Array.prototype @@ -25,6 +55,10 @@ export const XMLHttpRequest = global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined export const AbortController = global?.AbortController export const userAgent = navigator?.userAgent -export const assignableWindow: Window & typeof globalThis & Record = win ?? ({} as any) +export const assignableWindow: Window & + typeof globalThis & + Record & { + __PosthogExtensions__?: PosthogExtensions + } = win ?? ({} as any) export { win as window }