diff --git a/.eslintrc.js b/.eslintrc.js index f84ab25e1..bbdfa9260 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,6 +76,12 @@ module.exports = { node: true, }, }, + { + files: 'cypress/**/*', + globals: { + cy: true, + }, + }, ], root: true, } diff --git a/cypress/e2e/capture.cy.js b/cypress/e2e/capture.cy.js index 32ce4e727..1c08b30d1 100644 --- a/cypress/e2e/capture.cy.js +++ b/cypress/e2e/capture.cy.js @@ -25,7 +25,7 @@ describe('Event capture', () => { sessionRecording: given.sessionRecording, supportedCompression: given.supportedCompression, excludedDomains: [], - autocaptureExceptions: true, + autocaptureExceptions: false, }, }).as('decide') @@ -53,14 +53,6 @@ describe('Event capture', () => { cy.phCaptures().should('include', 'custom-event') }) - it('captures exceptions', () => { - start() - - cy.get('[data-cy-exception-button]').click() - cy.phCaptures().should('have.length', 3) - cy.phCaptures().should('include', '$exception') - }) - describe('autocapture config', () => { it('dont capture click when configured not to', () => { given('options', () => ({ diff --git a/rollup.config.js b/rollup.config.js index a05281819..0a6e1a0bb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -58,6 +58,18 @@ export default [ ], plugins: [...plugins], }, + { + input: 'src/loader-exception-autocapture.ts', + output: [ + { + file: 'dist/exception-autocapture.js', + sourcemap: true, + format: 'iife', + name: 'posthog', + }, + ], + plugins: [...plugins], + }, { input: 'src/loader-globals.ts', output: [ diff --git a/src/__tests__/extensions/exceptions/error-conversion.test.ts b/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts similarity index 99% rename from src/__tests__/extensions/exceptions/error-conversion.test.ts rename to src/__tests__/extensions/exception-autocapture/error-conversion.test.ts index 053e95eef..f74799e94 100644 --- a/src/__tests__/extensions/exceptions/error-conversion.test.ts +++ b/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts @@ -4,7 +4,7 @@ import { errorToProperties, ErrorProperties, unhandledRejectionToProperties, -} from '../../../extensions/exceptions/error-conversion' +} from '../../../extensions/exception-autocapture/error-conversion' import { _isNull } from '../../../utils' // ugh, jest diff --git a/src/__tests__/extensions/exceptions/exception-observer.test.ts b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts similarity index 98% rename from src/__tests__/extensions/exceptions/exception-observer.test.ts rename to src/__tests__/extensions/exception-autocapture/exception-observer.test.ts index 42833cf46..a85ac3118 100644 --- a/src/__tests__/extensions/exceptions/exception-observer.test.ts +++ b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { PostHog } from '../../../posthog-core' import { DecideResponse, PostHogConfig } from '../../../types' -import { ExceptionObserver } from '../../../extensions/exceptions/exception-autocapture' +import { ExceptionObserver } from '../../../extensions/exception-autocapture' +import { window } from '../../../utils' describe('Exception Observer', () => { let exceptionObserver: ExceptionObserver diff --git a/src/decide.ts b/src/decide.ts index e17d6016d..e265ee238 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -1,5 +1,5 @@ import { autocapture } from './autocapture' -import { _base64Encode, loadScript, logger } from './utils' +import { _base64Encode, _isUndefined, loadScript, logger } from './utils' import { PostHog } from './posthog-core' import { Compression, DecideResponse } from './types' import { STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY } from './constants' @@ -59,7 +59,6 @@ export class Decide { this.instance.sessionRecording?.afterDecideResponse(response) autocapture.afterDecideResponse(response, this.instance) this.instance.webPerformance?.afterDecideResponse(response) - this.instance.exceptionAutocapture?.afterDecideResponse(response) if (!this.instance.config.advanced_disable_feature_flags_on_first_load) { this.instance.featureFlags.receivedFeatureFlags(response) @@ -74,7 +73,6 @@ export class Decide { this.instance['compression'] = compression } - // Check if recorder.js is already loaded // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const surveysGenerator = window?.extendPostHogWithSurveys @@ -91,6 +89,25 @@ export class Decide { }) } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const exceptionAutoCaptureAddedToWindow = window?.extendPostHogWithExceptionAutoCapture + if ( + response['autocaptureExceptions'] && + !!response['autocaptureExceptions'] && + _isUndefined(exceptionAutoCaptureAddedToWindow) + ) { + loadScript(this.instance.config.api_host + `/static/exception-autocapture.js`, (err) => { + if (err) { + return logger.error(`Could not load exception autocapture script`, err) + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + window.extendPostHogWithExceptionAutocapture(this.instance, response) + }) + } + if (response['siteApps']) { if (this.instance.config.opt_in_site_apps) { const apiHost = this.instance.config.api_host diff --git a/src/extensions/exceptions/error-conversion.ts b/src/extensions/exception-autocapture/error-conversion.ts similarity index 100% rename from src/extensions/exceptions/error-conversion.ts rename to src/extensions/exception-autocapture/error-conversion.ts diff --git a/src/extensions/exceptions/exception-autocapture.ts b/src/extensions/exception-autocapture/index.ts similarity index 87% rename from src/extensions/exceptions/exception-autocapture.ts rename to src/extensions/exception-autocapture/index.ts index 4202a71e5..7994d8cb6 100644 --- a/src/extensions/exceptions/exception-autocapture.ts +++ b/src/extensions/exception-autocapture/index.ts @@ -1,4 +1,4 @@ -import { _isArray, _isUndefined, logger, window } from '../../utils' +import { _isArray, _isObject, _isUndefined, logger, window } from '../../utils' import { PostHog } from '../../posthog-core' import { DecideResponse, Properties } from '../../types' import { ErrorEventArgs, ErrorProperties, errorToProperties, unhandledRejectionToProperties } from './error-conversion' @@ -6,6 +6,12 @@ import { isPrimitive } from './type-checking' const EXCEPTION_INGESTION_ENDPOINT = '/e/' +export const extendPostHog = (instance: PostHog, response: DecideResponse) => { + const exceptionObserver = new ExceptionObserver(instance) + exceptionObserver.afterDecideResponse(response) + return exceptionObserver +} + export class ExceptionObserver { instance: PostHog remoteEnabled: boolean | undefined @@ -18,10 +24,6 @@ export class ExceptionObserver { this.instance = instance } - private debugLog(...args: any[]) { - logger.info('[ExceptionObserver]', ...args) - } - startCapturing() { if (!this.isEnabled() || (window.onerror as any)?.__POSTHOG_INSTRUMENTED__) { return @@ -104,7 +106,10 @@ export class ExceptionObserver { if (this.isEnabled()) { this.startCapturing() - this.debugLog('Remote config for exception autocapture is enabled, starting', autocaptureExceptionsResponse) + logger.info( + '[Exception Capture] Remote config for exception autocapture is enabled, starting with config: ', + _isObject(autocaptureExceptionsResponse) ? autocaptureExceptionsResponse : {} + ) } } @@ -112,7 +117,7 @@ export class ExceptionObserver { const errorProperties = errorToProperties(args) if (this.errorsToIgnore.some((regex) => regex.test(errorProperties.$exception_message || ''))) { - this.debugLog('Ignoring exception based on remote config', errorProperties) + logger.info('[Exception Capture] Ignoring exception based on remote config', errorProperties) return } diff --git a/src/extensions/exceptions/stack-trace.ts b/src/extensions/exception-autocapture/stack-trace.ts similarity index 100% rename from src/extensions/exceptions/stack-trace.ts rename to src/extensions/exception-autocapture/stack-trace.ts diff --git a/src/extensions/exceptions/type-checking.ts b/src/extensions/exception-autocapture/type-checking.ts similarity index 100% rename from src/extensions/exceptions/type-checking.ts rename to src/extensions/exception-autocapture/type-checking.ts diff --git a/src/extensions/sentry-integration.ts b/src/extensions/sentry-integration.ts index 388195379..914cf7a55 100644 --- a/src/extensions/sentry-integration.ts +++ b/src/extensions/sentry-integration.ts @@ -17,7 +17,6 @@ */ import { PostHog } from '../posthog-core' -import { ErrorProperties } from './exceptions/error-conversion' // NOTE - we can't import from @sentry/types because it changes frequently and causes clashes // We only use a small subset of the types, so we can just define the integration overall and use any for the rest @@ -73,7 +72,13 @@ export class SentryIntegration implements _SentryIntegration { const exceptions = event.exception?.values || [] - const data: SentryExceptionProperties & ErrorProperties = { + const data: SentryExceptionProperties & { + // two properties added to match any exception auto-capture + // added manually to avoid any dependency on the lazily loaded content + $exception_message: any + $exception_type: any + $exception_personURL: string + } = { // PostHog Exception Properties, $exception_message: exceptions[0]?.value, $exception_type: exceptions[0]?.type, diff --git a/src/loader-exception-autocapture.ts b/src/loader-exception-autocapture.ts new file mode 100644 index 000000000..64596eb57 --- /dev/null +++ b/src/loader-exception-autocapture.ts @@ -0,0 +1,8 @@ +import { extendPostHog } from './extensions/exception-autocapture' +import { _isUndefined } from './utils' + +const win: Window & typeof globalThis = _isUndefined(window) ? ({} as typeof window) : window + +;(win as any).extendPostHogWithExceptionAutoCapture = extendPostHog + +export default extendPostHog diff --git a/src/posthog-core.ts b/src/posthog-core.ts index a7a6507ca..e8f98c9bb 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -58,7 +58,6 @@ import { import { SentryIntegration } from './extensions/sentry-integration' import { createSegmentIntegration } from './extensions/segment-integration' import { PageViewManager } from './page-view' -import { ExceptionObserver } from './extensions/exceptions/exception-autocapture' import { PostHogSurveys } from './posthog-surveys' import { RateLimiter } from './rate-limiter' import { uuidv7 } from './uuidv7' @@ -224,8 +223,6 @@ const create_phlib = function ( instance.pageViewManager.startMeasuringScrollPosition() } - instance.exceptionAutocapture = new ExceptionObserver(instance) - instance.__autocapture = instance.config.autocapture autocapture._setIsAutocaptureEnabled(instance) if (autocapture._isAutocaptureEnabled) { @@ -284,7 +281,6 @@ export class PostHog { _retryQueue?: RetryQueue sessionRecording?: SessionRecording webPerformance?: WebPerformanceObserver - exceptionAutocapture?: ExceptionObserver _triggered_notifs: any compression: Partial> @@ -795,23 +791,6 @@ export class PostHog { this._execute_array([item]) } - /* - * PostHog supports exception autocapture, however, this function - * is used to manually capture an exception - * and can be used to add more context to that exception - * - * Properties passed as the second option will be merged with the properties - * of the exception event. - * Where there is a key in both generated exception and passed properties, - * the generated exception property takes precedence. - */ - captureException(exception: Error, properties?: Properties): void { - this.exceptionAutocapture?.captureException( - [exception.name, undefined, undefined, undefined, exception], - properties - ) - } - /** * Capture an event. This is the most important and * frequently used PostHog function.