posthog-node 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/index.ts +1 -0
  3. package/lib/index.cjs.js +911 -7
  4. package/lib/index.cjs.js.map +1 -1
  5. package/lib/index.d.ts +44 -3
  6. package/lib/index.esm.js +911 -7
  7. package/lib/index.esm.js.map +1 -1
  8. package/lib/posthog-core/src/index.d.ts +6 -0
  9. package/lib/posthog-core/src/types.d.ts +1 -0
  10. package/lib/posthog-core/src/utils.d.ts +2 -0
  11. package/lib/posthog-node/index.d.ts +1 -0
  12. package/lib/posthog-node/src/error-tracking.d.ts +12 -0
  13. package/lib/posthog-node/src/extensions/error-tracking/autocapture.d.ts +3 -0
  14. package/lib/posthog-node/src/extensions/error-tracking/context-lines.d.ts +4 -0
  15. package/lib/posthog-node/src/extensions/error-tracking/error-conversion.d.ts +5 -0
  16. package/lib/posthog-node/src/extensions/error-tracking/reduceable-cache.d.ts +12 -0
  17. package/lib/posthog-node/src/extensions/error-tracking/stack-trace.d.ts +15 -0
  18. package/lib/posthog-node/src/extensions/error-tracking/type-checking.d.ts +7 -0
  19. package/lib/posthog-node/src/extensions/error-tracking/types.d.ts +57 -0
  20. package/lib/posthog-node/src/extensions/express.d.ts +17 -0
  21. package/lib/posthog-node/src/extensions/sentry-integration.d.ts +1 -2
  22. package/lib/posthog-node/src/fetch.d.ts +1 -2
  23. package/lib/posthog-node/src/posthog-node.d.ts +5 -0
  24. package/package.json +1 -1
  25. package/src/error-tracking.ts +66 -0
  26. package/src/extensions/error-tracking/autocapture.ts +62 -0
  27. package/src/extensions/error-tracking/context-lines.ts +389 -0
  28. package/src/extensions/error-tracking/error-conversion.ts +250 -0
  29. package/src/extensions/error-tracking/reduceable-cache.ts +36 -0
  30. package/src/extensions/error-tracking/stack-trace.ts +269 -0
  31. package/src/extensions/error-tracking/type-checking.ts +37 -0
  32. package/src/extensions/error-tracking/types.ts +62 -0
  33. package/src/extensions/express.ts +37 -0
  34. package/src/extensions/sentry-integration.ts +1 -3
  35. package/src/fetch.ts +3 -7
  36. package/src/posthog-node.ts +10 -0
@@ -0,0 +1,269 @@
1
+ // copied and adapted from https://github.com/getsentry/sentry-javascript/blob/41fef4b10f3a644179b77985f00f8696c908539f/packages/browser/src/stack-parsers.ts
2
+ // 💖open source
3
+
4
+ import { posix, sep, dirname } from 'node:path'
5
+ import { StackFrame, StackLineParser, StackLineParserFn, StackParser } from './types'
6
+
7
+ type GetModuleFn = (filename: string | undefined) => string | undefined
8
+
9
+ // This was originally forked from https://github.com/csnover/TraceKit, and was largely
10
+ // re-written as part of raven - js.
11
+ //
12
+ // This code was later copied to the JavaScript mono - repo and further modified and
13
+ // refactored over the years.
14
+
15
+ // Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors.
16
+ //
17
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this
18
+ // software and associated documentation files(the 'Software'), to deal in the Software
19
+ // without restriction, including without limitation the rights to use, copy, modify,
20
+ // merge, publish, distribute, sublicense, and / or sell copies of the Software, and to
21
+ // permit persons to whom the Software is furnished to do so, subject to the following
22
+ // conditions:
23
+ //
24
+ // The above copyright notice and this permission notice shall be included in all copies
25
+ // or substantial portions of the Software.
26
+ //
27
+ // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
28
+ // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
29
+ // PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30
+ // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
31
+ // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
32
+ // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33
+
34
+ const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/
35
+ const STACKTRACE_FRAME_LIMIT = 50
36
+
37
+ const UNKNOWN_FUNCTION = '?'
38
+
39
+ /** Node Stack line parser */
40
+ export function node(getModule?: GetModuleFn): StackLineParserFn {
41
+ const FILENAME_MATCH = /^\s*[-]{4,}$/
42
+ const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/
43
+
44
+ return (line: string) => {
45
+ const lineMatch = line.match(FULL_MATCH)
46
+
47
+ if (lineMatch) {
48
+ let object: string | undefined
49
+ let method: string | undefined
50
+ let functionName: string | undefined
51
+ let typeName: string | undefined
52
+ let methodName: string | undefined
53
+
54
+ if (lineMatch[1]) {
55
+ functionName = lineMatch[1]
56
+
57
+ let methodStart = functionName.lastIndexOf('.')
58
+ if (functionName[methodStart - 1] === '.') {
59
+ methodStart--
60
+ }
61
+
62
+ if (methodStart > 0) {
63
+ object = functionName.slice(0, methodStart)
64
+ method = functionName.slice(methodStart + 1)
65
+ const objectEnd = object.indexOf('.Module')
66
+ if (objectEnd > 0) {
67
+ functionName = functionName.slice(objectEnd + 1)
68
+ object = object.slice(0, objectEnd)
69
+ }
70
+ }
71
+ typeName = undefined
72
+ }
73
+
74
+ if (method) {
75
+ typeName = object
76
+ methodName = method
77
+ }
78
+
79
+ if (method === '<anonymous>') {
80
+ methodName = undefined
81
+ functionName = undefined
82
+ }
83
+
84
+ if (functionName === undefined) {
85
+ methodName = methodName || UNKNOWN_FUNCTION
86
+ functionName = typeName ? `${typeName}.${methodName}` : methodName
87
+ }
88
+
89
+ let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]
90
+ const isNative = lineMatch[5] === 'native'
91
+
92
+ // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
93
+ if (filename?.match(/\/[A-Z]:/)) {
94
+ filename = filename.slice(1)
95
+ }
96
+
97
+ if (!filename && lineMatch[5] && !isNative) {
98
+ filename = lineMatch[5]
99
+ }
100
+
101
+ return {
102
+ filename: filename ? decodeURI(filename) : undefined,
103
+ module: getModule ? getModule(filename) : undefined,
104
+ function: functionName,
105
+ lineno: _parseIntOrUndefined(lineMatch[3]),
106
+ colno: _parseIntOrUndefined(lineMatch[4]),
107
+ in_app: filenameIsInApp(filename || '', isNative),
108
+ platform: 'node:javascript',
109
+ }
110
+ }
111
+
112
+ if (line.match(FILENAME_MATCH)) {
113
+ return {
114
+ filename: line,
115
+ platform: 'node:javascript',
116
+ }
117
+ }
118
+
119
+ return undefined
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Does this filename look like it's part of the app code?
125
+ */
126
+ export function filenameIsInApp(filename: string, isNative: boolean = false): boolean {
127
+ const isInternal =
128
+ isNative ||
129
+ (filename &&
130
+ // It's not internal if it's an absolute linux path
131
+ !filename.startsWith('/') &&
132
+ // It's not internal if it's an absolute windows path
133
+ !filename.match(/^[A-Z]:/) &&
134
+ // It's not internal if the path is starting with a dot
135
+ !filename.startsWith('.') &&
136
+ // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack
137
+ !filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//)) // Schema from: https://stackoverflow.com/a/3641782
138
+
139
+ // in_app is all that's not an internal Node function or a module within node_modules
140
+ // note that isNative appears to return true even for node core libraries
141
+ // see https://github.com/getsentry/raven-node/issues/176
142
+
143
+ return !isInternal && filename !== undefined && !filename.includes('node_modules/')
144
+ }
145
+
146
+ function _parseIntOrUndefined(input: string | undefined): number | undefined {
147
+ return parseInt(input || '', 10) || undefined
148
+ }
149
+
150
+ export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser {
151
+ return [90, node(getModule)]
152
+ }
153
+
154
+ export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename()))
155
+
156
+ /** Creates a function that gets the module name from a filename */
157
+ export function createGetModuleFromFilename(
158
+ basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(),
159
+ isWindows: boolean = sep === '\\'
160
+ ): (filename: string | undefined) => string | undefined {
161
+ const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath
162
+
163
+ return (filename: string | undefined) => {
164
+ if (!filename) {
165
+ return
166
+ }
167
+
168
+ const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename
169
+
170
+ // eslint-disable-next-line prefer-const
171
+ let { dir, base: file, ext } = posix.parse(normalizedFilename)
172
+
173
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
174
+ file = file.slice(0, ext.length * -1)
175
+ }
176
+
177
+ // The file name might be URI-encoded which we want to decode to
178
+ // the original file name.
179
+ const decodedFile = decodeURIComponent(file)
180
+
181
+ if (!dir) {
182
+ // No dirname whatsoever
183
+ dir = '.'
184
+ }
185
+
186
+ const n = dir.lastIndexOf('/node_modules')
187
+ if (n > -1) {
188
+ return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`
189
+ }
190
+
191
+ // Let's see if it's a part of the main module
192
+ // To be a part of main module, it has to share the same base
193
+ if (dir.startsWith(normalizedBase)) {
194
+ const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.')
195
+ return moduleName ? `${moduleName}:${decodedFile}` : decodedFile
196
+ }
197
+
198
+ return decodedFile
199
+ }
200
+ }
201
+
202
+ /** normalizes Windows paths */
203
+ function normalizeWindowsPath(path: string): string {
204
+ return path
205
+ .replace(/^[A-Z]:/, '') // remove Windows-style prefix
206
+ .replace(/\\/g, '/') // replace all `\` instances with `/`
207
+ }
208
+
209
+ export function createStackParser(...parsers: StackLineParser[]): StackParser {
210
+ const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map((p) => p[1])
211
+
212
+ return (stack: string, skipFirstLines: number = 0): StackFrame[] => {
213
+ const frames: StackFrame[] = []
214
+ const lines = stack.split('\n')
215
+
216
+ for (let i = skipFirstLines; i < lines.length; i++) {
217
+ const line = lines[i] as string
218
+ // Ignore lines over 1kb as they are unlikely to be stack frames.
219
+ if (line.length > 1024) {
220
+ continue
221
+ }
222
+
223
+ // https://github.com/getsentry/sentry-javascript/issues/5459
224
+ // Remove webpack (error: *) wrappers
225
+ const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line
226
+
227
+ // https://github.com/getsentry/sentry-javascript/issues/7813
228
+ // Skip Error: lines
229
+ if (cleanedLine.match(/\S*Error: /)) {
230
+ continue
231
+ }
232
+
233
+ for (const parser of sortedParsers) {
234
+ const frame = parser(cleanedLine)
235
+
236
+ if (frame) {
237
+ frames.push(frame)
238
+ break
239
+ }
240
+ }
241
+
242
+ if (frames.length >= STACKTRACE_FRAME_LIMIT) {
243
+ break
244
+ }
245
+ }
246
+
247
+ return reverseAndStripFrames(frames)
248
+ }
249
+ }
250
+
251
+ export function reverseAndStripFrames(stack: ReadonlyArray<StackFrame>): StackFrame[] {
252
+ if (!stack.length) {
253
+ return []
254
+ }
255
+
256
+ const localStack = Array.from(stack)
257
+
258
+ localStack.reverse()
259
+
260
+ return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map((frame) => ({
261
+ ...frame,
262
+ filename: frame.filename || getLastStackFrame(localStack).filename,
263
+ function: frame.function || UNKNOWN_FUNCTION,
264
+ }))
265
+ }
266
+
267
+ function getLastStackFrame(arr: StackFrame[]): StackFrame {
268
+ return arr[arr.length - 1] || {}
269
+ }
@@ -0,0 +1,37 @@
1
+ import { PolymorphicEvent } from './types'
2
+
3
+ export function isEvent(candidate: unknown): candidate is PolymorphicEvent {
4
+ return typeof Event !== 'undefined' && isInstanceOf(candidate, Event)
5
+ }
6
+
7
+ export function isPlainObject(candidate: unknown): candidate is Record<string, unknown> {
8
+ return isBuiltin(candidate, 'Object')
9
+ }
10
+
11
+ export function isError(candidate: unknown): candidate is Error {
12
+ switch (Object.prototype.toString.call(candidate)) {
13
+ case '[object Error]':
14
+ case '[object Exception]':
15
+ case '[object DOMException]':
16
+ case '[object WebAssembly.Exception]':
17
+ return true
18
+ default:
19
+ return isInstanceOf(candidate, Error)
20
+ }
21
+ }
22
+
23
+ export function isInstanceOf(candidate: unknown, base: any): boolean {
24
+ try {
25
+ return candidate instanceof base
26
+ } catch {
27
+ return false
28
+ }
29
+ }
30
+
31
+ export function isErrorEvent(event: unknown): boolean {
32
+ return isBuiltin(event, 'ErrorEvent')
33
+ }
34
+
35
+ export function isBuiltin(candidate: unknown, className: string): boolean {
36
+ return Object.prototype.toString.call(candidate) === `[object ${className}]`
37
+ }
@@ -0,0 +1,62 @@
1
+ // levels originally copied from Sentry to work with the sentry integration
2
+ // and to avoid relying on a frequently changing @sentry/types dependency
3
+ // but provided as an array of literal types, so we can constrain the level below
4
+ export const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const
5
+ export declare type SeverityLevel = (typeof severityLevels)[number]
6
+
7
+ export interface PolymorphicEvent {
8
+ [key: string]: unknown
9
+ readonly type: string
10
+ readonly target?: unknown
11
+ readonly currentTarget?: unknown
12
+ }
13
+
14
+ export interface EventHint {
15
+ mechanism?: Partial<Mechanism>
16
+ syntheticException?: Error | null
17
+ }
18
+
19
+ export interface ErrorProperties {
20
+ $exception_list: Exception[]
21
+ $exception_level?: SeverityLevel
22
+ $exception_DOMException_code?: string
23
+ $exception_personURL?: string
24
+ }
25
+
26
+ export interface Exception {
27
+ type?: string
28
+ value?: string
29
+ mechanism?: Mechanism
30
+ module?: string
31
+ thread_id?: number
32
+ stacktrace?: { frames?: StackFrame[]; type: 'raw' }
33
+ }
34
+
35
+ export interface Mechanism {
36
+ handled?: boolean
37
+ type?: string
38
+ source?: string
39
+ synthetic?: boolean
40
+ }
41
+
42
+ export type StackParser = (stack: string, skipFirstLines?: number) => StackFrame[]
43
+ export type StackLineParserFn = (line: string) => StackFrame | undefined
44
+ export type StackLineParser = [number, StackLineParserFn]
45
+
46
+ export interface StackFrame {
47
+ platform: string
48
+ filename?: string
49
+ function?: string
50
+ module?: string
51
+ lineno?: number
52
+ colno?: number
53
+ abs_path?: string
54
+ context_line?: string
55
+ pre_context?: string[]
56
+ post_context?: string[]
57
+ in_app?: boolean
58
+ instruction_addr?: string
59
+ addr_mode?: string
60
+ vars?: { [key: string]: any }
61
+ debug_id?: string
62
+ }
@@ -0,0 +1,37 @@
1
+ import type * as http from 'node:http'
2
+ import { uuidv7 } from 'posthog-core/src/vendor/uuidv7'
3
+ import ErrorTracking from '../error-tracking'
4
+ import { PostHog } from '../posthog-node'
5
+
6
+ type ExpressMiddleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void
7
+
8
+ type ExpressErrorMiddleware = (
9
+ error: MiddlewareError,
10
+ req: http.IncomingMessage,
11
+ res: http.ServerResponse,
12
+ next: (error: MiddlewareError) => void
13
+ ) => void
14
+
15
+ interface MiddlewareError extends Error {
16
+ status?: number | string
17
+ statusCode?: number | string
18
+ status_code?: number | string
19
+ output?: {
20
+ statusCode?: number | string
21
+ }
22
+ }
23
+
24
+ export function setupExpressErrorHandler(
25
+ _posthog: PostHog,
26
+ app: {
27
+ use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown
28
+ }
29
+ ): void {
30
+ app.use((error: MiddlewareError, _, __, next: (error: MiddlewareError) => void): void => {
31
+ const hint = { mechanism: { type: 'middleware', handled: false } }
32
+ // Given stateless nature of Node SDK we capture exceptions using personless processing
33
+ // when no user can be determined e.g. in the case of exception autocapture
34
+ ErrorTracking.captureException(_posthog, error, uuidv7(), hint, { $process_person_profile: false })
35
+ next(error)
36
+ })
37
+ }
@@ -22,11 +22,9 @@
22
22
  * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
23
23
  */
24
24
 
25
+ import { SeverityLevel } from 'posthog-node/src/extensions/error-tracking/types'
25
26
  import { type PostHog } from '../posthog-node'
26
27
 
27
- export const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const
28
- export declare type SeverityLevel = (typeof severityLevels)[number]
29
-
30
28
  // NOTE - we can't import from @sentry/types because it changes frequently and causes clashes
31
29
  // We only use a small subset of the types, so we can just define the integration overall and use any for the rest
32
30
 
package/src/fetch.ts CHANGED
@@ -7,14 +7,10 @@
7
7
  * See https://github.com/PostHog/posthog-js-lite/issues/127 for more info
8
8
  */
9
9
 
10
- import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
10
+ import { FetchLike, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
11
+ import { getFetch } from 'posthog-core/src/utils'
11
12
 
12
- type FetchLike = (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
13
-
14
- let _fetch: FetchLike | undefined =
15
- // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
16
- // @ts-ignore
17
- typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined
13
+ let _fetch: FetchLike | undefined = getFetch()
18
14
 
19
15
  if (!_fetch) {
20
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -14,10 +14,13 @@ import { PostHogMemoryStorage } from '../../posthog-core/src/storage-memory'
14
14
  import { EventMessage, GroupIdentifyMessage, IdentifyMessage, PostHogNodeV1 } from './types'
15
15
  import { FeatureFlagsPoller } from './feature-flags'
16
16
  import fetch from './fetch'
17
+ import ErrorTracking from './error-tracking'
17
18
 
18
19
  export type PostHogOptions = PostHogCoreOptions & {
19
20
  persistence?: 'memory'
20
21
  personalApiKey?: string
22
+ privacyMode?: boolean
23
+ enableExceptionAutocapture?: boolean
21
24
  // The interval in milliseconds between polls for refreshing feature flag definitions. Defaults to 30 seconds.
22
25
  featureFlagsPollingInterval?: number
23
26
  // Maximum size of cache that deduplicates $feature_flag_called calls per user.
@@ -33,6 +36,7 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
33
36
  private _memoryStorage = new PostHogMemoryStorage()
34
37
 
35
38
  private featureFlagsPoller?: FeatureFlagsPoller
39
+ protected errorTracking: ErrorTracking
36
40
  private maxCacheSize: number
37
41
  public readonly options: PostHogOptions
38
42
 
@@ -60,6 +64,7 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
60
64
  customHeaders: this.getCustomHeaders(),
61
65
  })
62
66
  }
67
+ this.errorTracking = new ErrorTracking(this, options)
63
68
  this.distinctIdHasSentFlagCalls = {}
64
69
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE
65
70
  }
@@ -480,4 +485,9 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
480
485
 
481
486
  return { allPersonProperties, allGroupProperties }
482
487
  }
488
+
489
+ captureException(error: unknown, distinctId: string, additionalProperties?: Record<string | number, any>): void {
490
+ const syntheticException = new Error('PostHog syntheticException')
491
+ ErrorTracking.captureException(this, error, distinctId, { syntheticException }, additionalProperties)
492
+ }
483
493
  }