diff --git a/package.json b/package.json index 596e94525..a67149679 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@rrweb/types": "2.0.0-alpha.13", - "@sentry/types": "7.37.2", + "@sentry/types": "8.7.0", "@testing-library/dom": "^9.3.0", "@types/eslint": "^8.44.6", "@types/jest": "^29.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe32c36bc..fab2a43da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -52,8 +52,8 @@ devDependencies: specifier: 2.0.0-alpha.13 version: 2.0.0-alpha.13 '@sentry/types': - specifier: 7.37.2 - version: 7.37.2 + specifier: 8.7.0 + version: 8.7.0 '@testing-library/dom': specifier: ^9.3.0 version: 9.3.0 @@ -2664,9 +2664,9 @@ packages: rrweb-snapshot: 2.0.0-alpha.13 dev: true - /@sentry/types@7.37.2: - resolution: {integrity: sha512-SxKQOCX94ZaQM4C2ysNjHdJsjYapu/NYZCz1cnPyCdDvYfhwiVge1uq6ZHiQ/ARfxAAOmc3R4Mh3VvEz7WUOdw==} - engines: {node: '>=8'} + /@sentry/types@8.7.0: + resolution: {integrity: sha512-11KLOKumP6akugVGLvSoEig+JlP0ZEzW3nN9P+ppgdIx9HAxMIh6UvumbieG4/DWjAh2kh6NPNfUw3gk2Gfq1A==} + engines: {node: '>=14.18'} dev: true /@sinclair/typebox@0.25.24: diff --git a/src/extensions/sentry-integration.ts b/src/extensions/sentry-integration.ts index 1ac9d6f58..980eedf5e 100644 --- a/src/extensions/sentry-integration.ts +++ b/src/extensions/sentry-integration.ts @@ -27,6 +27,7 @@ import { PostHog } from '../posthog-core' // Hub as _SentryHub, // Integration as _SentryIntegration, // SeverityLevel as _SeverityLevel, +// IntegrationClass as _SentryIntegrationClass, // } from '@sentry/types' // Uncomment the above and comment the below to get type checking for development @@ -35,11 +36,16 @@ type _SentryEvent = any type _SentryEventProcessor = any type _SentryHub = any -interface _SentryIntegration { +interface _SentryIntegrationClass { name: string setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void } +interface _SentryIntegration { + name: string + processEvent(event: _SentryEvent): _SentryEvent +} + // levels copied from Sentry to avoid relying on a frequently changing @sentry/types dependency // but provided as an array of literal types, so we can constrain the level below const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const @@ -54,7 +60,90 @@ interface SentryExceptionProperties { $sentry_url?: string } -export class SentryIntegration implements _SentryIntegration { +export type SentryIntegrationOptions = { + organization?: string + projectId?: number + prefix?: string + /** + * By default, only errors are sent to PostHog. You can set this to '*' to send all events. + * Or to an error of SeverityLevel to only send events matching the provided levels. + * e.g. ['error', 'fatal'] to send only errors and fatals + * e.g. ['error'] to send only errors -- the default when omitted + * e.g. '*' to send all events + */ + severityAllowList?: _SeverityLevel[] | '*' +} + +const NAME = 'posthog-js' + +export function createEventProcessor( + _posthog: PostHog, + { organization, projectId, prefix, severityAllowList = ['error'] }: SentryIntegrationOptions = {} +): (event: _SentryEvent) => _SentryEvent { + return (event) => { + const shouldProcessLevel = + severityAllowList === '*' || severityAllowList.includes(event.level as _SeverityLevel) + if (!shouldProcessLevel || !_posthog.__loaded) return event + if (!event.tags) event.tags = {} + + const personUrl = _posthog.requestRouter.endpointFor( + 'ui', + `/project/${_posthog.config.token}/person/${_posthog.get_distinct_id()}` + ) + event.tags['PostHog Person URL'] = personUrl + if (_posthog.sessionRecordingStarted()) { + event.tags['PostHog Recording URL'] = _posthog.get_session_replay_url({ withTimestamp: true }) + } + + const exceptions = event.exception?.values || [] + + 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 + $level: _SeverityLevel + } = { + // PostHog Exception Properties, + $exception_message: exceptions[0]?.value || event.message, + $exception_type: exceptions[0]?.type, + $exception_personURL: personUrl, + // Sentry Exception Properties + $sentry_event_id: event.event_id, + $sentry_exception: event.exception, + $sentry_exception_message: exceptions[0]?.value || event.message, + $sentry_exception_type: exceptions[0]?.type, + $sentry_tags: event.tags, + $level: event.level, + } + + if (organization && projectId) { + data['$sentry_url'] = + (prefix || 'https://sentry.io/organizations/') + + organization + + '/issues/?project=' + + projectId + + '&query=' + + event.event_id + } + _posthog.capture('$exception', data) + return event + } +} + +// V8 integration - function based +export function sentryIntegration(_posthog: PostHog, options?: SentryIntegrationOptions): _SentryIntegration { + const processor = createEventProcessor(_posthog, options) + return { + name: NAME, + processEvent(event) { + return processor(event) + }, + } +} +// V7 integration - class based +export class SentryIntegration implements _SentryIntegrationClass { name: string setupOnce: ( @@ -74,60 +163,14 @@ export class SentryIntegration implements _SentryIntegration { * e.g. ['error'] to send only errors -- the default when omitted * e.g. '*' to send all events */ - severityAllowList: _SeverityLevel[] | '*' = ['error'] + severityAllowList?: _SeverityLevel[] | '*' ) { // setupOnce gets called by Sentry when it intializes the plugin - this.name = 'posthog-js' + this.name = NAME this.setupOnce = function (addGlobalEventProcessor: (callback: _SentryEventProcessor) => void) { - addGlobalEventProcessor((event: _SentryEvent) => { - const shouldProcessLevel = - severityAllowList === '*' || severityAllowList.includes(event.level as _SeverityLevel) - if (!shouldProcessLevel || !_posthog.__loaded) return event - if (!event.tags) event.tags = {} - - const personUrl = _posthog.requestRouter.endpointFor( - 'ui', - `/project/${_posthog.config.token}/person/${_posthog.get_distinct_id()}` - ) - event.tags['PostHog Person URL'] = personUrl - if (_posthog.sessionRecordingStarted()) { - event.tags['PostHog Recording URL'] = _posthog.get_session_replay_url({ withTimestamp: true }) - } - - const exceptions = event.exception?.values || [] - - 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 - $level: _SeverityLevel - } = { - // PostHog Exception Properties, - $exception_message: exceptions[0]?.value || event.message, - $exception_type: exceptions[0]?.type, - $exception_personURL: personUrl, - // Sentry Exception Properties - $sentry_event_id: event.event_id, - $sentry_exception: event.exception, - $sentry_exception_message: exceptions[0]?.value || event.message, - $sentry_exception_type: exceptions[0]?.type, - $sentry_tags: event.tags, - $level: event.level, - } - - if (organization && projectId) - data['$sentry_url'] = - (prefix || 'https://sentry.io/organizations/') + - organization + - '/issues/?project=' + - projectId + - '&query=' + - event.event_id - _posthog.capture('$exception', data) - return event - }) + addGlobalEventProcessor( + createEventProcessor(_posthog, { organization, projectId, prefix, severityAllowList }) + ) } } } diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 680a4144b..39260e611 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -46,7 +46,7 @@ import { SnippetArrayItem, ToolbarParams, } from './types' -import { SentryIntegration } from './extensions/sentry-integration' +import { SentryIntegration, SentryIntegrationOptions, sentryIntegration } from './extensions/sentry-integration' import { setupSegmentIntegration } from './extensions/segment-integration' import { PageViewManager } from './page-view' import { PostHogSurveys } from './posthog-surveys' @@ -262,6 +262,7 @@ export class PostHog { analyticsDefaultEndpoint: string SentryIntegration: typeof SentryIntegration + sentryIntegration: (options?: SentryIntegrationOptions) => ReturnType private _debugEventEmitter = new SimpleEventEmitter() @@ -276,6 +277,7 @@ export class PostHog { this.decideEndpointWasHit = false this.SentryIntegration = SentryIntegration + this.sentryIntegration = (options?: SentryIntegrationOptions) => sentryIntegration(this, options) this.__request_queue = [] this.__loaded = false this.analyticsDefaultEndpoint = '/e/'