Skip to content

Commit

Permalink
feat: Sentry integration (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Jan 2, 2024
1 parent 2c87234 commit cca013a
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 12 deletions.
1 change: 1 addition & 0 deletions examples/example-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"example": "ts-node example.ts"
},
"dependencies": {
"@sentry/node": "^7.91.0",
"express": "^4.18.1",
"posthog-node": "file:.yalc/posthog-node",
"undici": "^5.8.0"
Expand Down
18 changes: 17 additions & 1 deletion examples/example-node/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import express from 'express'
import { PostHog } from 'posthog-node'
import { PostHog, PostHogSentryIntegration } from 'posthog-node'
import undici from 'undici'

import * as Sentry from '@sentry/node'

const app = express()

const {
Expand All @@ -23,11 +25,25 @@ const posthog = new PostHog(PH_API_KEY, {

posthog.debug()

Sentry.init({
dsn: 'https://[email protected]/0',
integrations: [new PostHogSentryIntegration(posthog)],
})

app.get('/', (req, res) => {
posthog.capture({ distinctId: 'EXAMPLE_APP_GLOBAL', event: 'legacy capture' })
res.send({ hello: 'world' })
})

app.get('/error', (req, res) => {
Sentry.captureException(new Error('example error'), {
tags: {
[PostHogSentryIntegration.POSTHOG_ID_TAG]: 'EXAMPLE_APP_GLOBAL',
},
})
res.send({ status: 'error!!' })
})

app.get('/user/:userId/action', (req, res) => {
posthog.capture({ distinctId: req.params.userId, event: 'user did action', properties: req.params })

Expand Down
69 changes: 62 additions & 7 deletions examples/example-node/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,46 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"

"@sentry-internal/[email protected]":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.91.0.tgz#fbb6e1e3383e1eeee08633384e004da73ac1c37d"
integrity sha512-JH5y6gs6BS0its7WF2DhySu7nkhPDfZcdpAXldxzIlJpqFkuwQKLU5nkYJpiIyZz1NHYYtW5aum2bV2oCOdDRA==
dependencies:
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"

"@sentry/[email protected]":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.91.0.tgz#229334d7f03dd5d90a17495e61ce4215ab730b2a"
integrity sha512-tu+gYq4JrTdrR+YSh5IVHF0fJi/Pi9y0HZ5H9HnYy+UMcXIotxf6hIEaC6ZKGeLWkGXffz2gKpQLe/g6vy/lPA==
dependencies:
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"

"@sentry/node@^7.91.0":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.91.0.tgz#26bf13c3daf988f9725afd1a3cc38ba2ff90d62a"
integrity sha512-hTIfSQxD7L+AKIqyjoq8CWBRkEQrrMZmA3GSZgPI5JFWBHgO0HBo5TH/8TU81oEJh6kqqHAl2ObMhmcnaFqlzg==
dependencies:
"@sentry-internal/tracing" "7.91.0"
"@sentry/core" "7.91.0"
"@sentry/types" "7.91.0"
"@sentry/utils" "7.91.0"
https-proxy-agent "^5.0.0"

"@sentry/[email protected]":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.91.0.tgz#5b68954e08986fecb0d4bef168df58eef62c32c7"
integrity sha512-bcQnb7J3P3equbCUc+sPuHog2Y47yGD2sCkzmnZBjvBT0Z1B4f36fI/5WjyZhTjLSiOdg3F2otwvikbMjmBDew==

"@sentry/[email protected]":
version "7.91.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.91.0.tgz#3b1a94c053c885877908cd3e1365e3d23e21a73f"
integrity sha512-fvxjrEbk6T6Otu++Ax9ntlQ0sGRiwSC179w68aC3u26Wr30FAIRKqHTCCdc2jyWk7Gd9uWRT/cq+g8NG/8BfSg==
dependencies:
"@sentry/types" "7.91.0"

"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
Expand Down Expand Up @@ -248,6 +288,13 @@ acorn@^8.4.1, acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==

agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"

ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
Expand Down Expand Up @@ -290,10 +337,10 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==

axios@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
axios@^1.6.2:
version "1.6.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.3.tgz#7f50f23b3aa246eff43c54834272346c396613f4"
integrity sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
Expand Down Expand Up @@ -424,7 +471,7 @@ [email protected]:
dependencies:
ms "2.0.0"

debug@^4.1.1, debug@^4.3.2:
debug@4, debug@^4.1.1, debug@^4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
Expand Down Expand Up @@ -810,6 +857,14 @@ [email protected]:
statuses "2.0.1"
toidentifier "1.0.1"

https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"

[email protected]:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
Expand Down Expand Up @@ -1071,9 +1126,9 @@ [email protected]:
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==

"posthog-node@file:.yalc/posthog-node":
version "3.1.2"
version "3.2.1"
dependencies:
axios "^1.6.0"
axios "^1.6.2"
rusha "^0.8.14"

prelude-ls@^1.2.1:
Expand Down
4 changes: 4 additions & 0 deletions posthog-node/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 3.3.0 - 2024-01-02

1. Adds PostHogSentryIntegration to allow automatic capturing of exceptions reported via the @sentry/node package

# 3.2.1 - 2023-12-15

1. Fixes issue where a background refresh of feature flags could throw an unhandled error. It now emits to be detected by `.on('error', ...)`
Expand Down
1 change: 1 addition & 0 deletions posthog-node/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './src/posthog-node'
export * from './src/extensions/sentry-integration'
6 changes: 3 additions & 3 deletions posthog-node/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "posthog-node",
"version": "3.2.1",
"version": "3.3.0",
"description": "PostHog Node.js integration",
"repository": {
"type" : "git",
"url" : "https://github.com/PostHog/posthog-js-lite.git",
"type": "git",
"url": "https://github.com/PostHog/posthog-js-lite.git",
"directory": "posthog-node"
},
"scripts": {
Expand Down
125 changes: 125 additions & 0 deletions posthog-node/src/extensions/sentry-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
*/
import { type PostHog } from '../posthog-node'

// 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

// import {
// Event as _SentryEvent,
// EventProcessor as _SentryEventProcessor,
// Exception as _SentryException,
// Hub as _SentryHub,
// Integration as _SentryIntegration,
// Primitive as _SentryPrimitive,
// } from '@sentry/types'

// Uncomment the above and comment the below to get type checking for development

type _SentryEvent = any
type _SentryEventProcessor = any
type _SentryHub = any
type _SentryException = any
type _SentryPrimitive = any

interface _SentryIntegration {
name: string
setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void
}

interface PostHogSentryExceptionProperties {
$sentry_event_id?: string
$sentry_exception?: { values?: _SentryException[] }
$sentry_exception_message?: string
$sentry_exception_type?: string
$sentry_tags: { [key: string]: _SentryPrimitive }
$sentry_url?: string
$exception_type?: string
$exception_message?: string
$exception_personURL?: string
}

/**
* Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
*
* ### Usage
*
* Sentry.init({
* dsn: 'https://example',
* integrations: [
* new PostHogSentryIntegration(posthog)
* ]
* })
*
* Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
*
* @param {Object} [posthog] The posthog object
* @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
* @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
* @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
*/
export class PostHogSentryIntegration implements _SentryIntegration {
public readonly name = 'posthog-node'

public static readonly POSTHOG_ID_TAG = 'posthog_distinct_id'

public constructor(
private readonly posthog: PostHog,
private readonly posthogHost?: string,
private readonly organization?: string,
private readonly prefix?: string
) {
this.posthogHost = posthog.options.host ?? 'https://app.posthog.com'
}

public setupOnce(
addGlobalEventProcessor: (callback: _SentryEventProcessor) => void,
getCurrentHub: () => _SentryHub
): void {
addGlobalEventProcessor((event: _SentryEvent): _SentryEvent => {
if (event.exception?.values === undefined || event.exception.values.length === 0) {
return event
}

if (!event.tags) {
event.tags = {}
}

const sentry = getCurrentHub()

// Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG]
if (userId === undefined) {
// If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
return event
}

event.tags['PostHog Person URL'] = new URL(`/person/${userId}`, this.posthogHost).toString()

const properties: PostHogSentryExceptionProperties = {
// PostHog Exception Properties
$exception_message: event.exception.values[0]?.value,
$exception_type: event.exception.values[0]?.type,
$exception_personURL: event.tags['PostHog Person URL'],
// Sentry Exception Properties
$sentry_event_id: event.event_id,
$sentry_exception: event.exception,
$sentry_exception_message: event.exception.values[0]?.value,
$sentry_exception_type: event.exception.values[0]?.type,
$sentry_tags: event.tags,
}

const projectId = sentry.getClient()?.getDsn()?.projectId
if (this.organization !== undefined && projectId !== undefined && event.event_id !== undefined) {
properties.$sentry_url = `${this.prefix ?? 'https://sentry.io/organizations'}/${
this.organization
}/issues/?project=${projectId}&query=${event.event_id}`
}

this.posthog.capture({ event: '$exception', distinctId: userId, properties })

return event
})
}
}
2 changes: 1 addition & 1 deletion posthog-node/src/posthog-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {

private featureFlagsPoller?: FeatureFlagsPoller
private maxCacheSize: number
private options: PostHogOptions
public readonly options: PostHogOptions

distinctIdHasSentFlagCalls: Record<string, string[]>

Expand Down
Loading

0 comments on commit cca013a

Please sign in to comment.