@posthog/ai 4.2.1 → 4.3.1

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.
@@ -14,7 +14,7 @@ interface ClientOptions {
14
14
  posthogModelOverride?: string
15
15
  posthogProviderOverride?: string
16
16
  posthogCostOverride?: CostOverride
17
- fullDebug?: boolean
17
+ posthogCaptureImmediate?: boolean
18
18
  }
19
19
 
20
20
  interface CreateInstrumentationMiddlewareOptions {
@@ -26,7 +26,7 @@ interface CreateInstrumentationMiddlewareOptions {
26
26
  posthogModelOverride?: string
27
27
  posthogProviderOverride?: string
28
28
  posthogCostOverride?: CostOverride
29
- fullDebug?: boolean
29
+ posthogCaptureImmediate?: boolean
30
30
  }
31
31
 
32
32
  interface PostHogInput {
@@ -128,7 +128,9 @@ const mapVercelPrompt = (prompt: LanguageModelV1Prompt): PostHogInput[] => {
128
128
  // Trim the inputs array until its JSON size fits within MAX_OUTPUT_SIZE
129
129
  let serialized = JSON.stringify(inputs)
130
130
  let removedCount = 0
131
- while (Buffer.byteLength(serialized, 'utf8') > MAX_OUTPUT_SIZE && inputs.length > 0) {
131
+ // We need to keep track of the initial size of the inputs array because we're going to be mutating it
132
+ let initialSize = inputs.length
133
+ for (let i = 0; i < initialSize && Buffer.byteLength(serialized, 'utf8') > MAX_OUTPUT_SIZE; i++) {
132
134
  inputs.shift()
133
135
  removedCount++
134
136
  serialized = JSON.stringify(inputs)
@@ -136,7 +138,7 @@ const mapVercelPrompt = (prompt: LanguageModelV1Prompt): PostHogInput[] => {
136
138
  if (removedCount > 0) {
137
139
  // Add one placeholder to indicate how many were removed
138
140
  inputs.unshift({
139
- role: 'assistant',
141
+ role: 'posthog',
140
142
  content: `[${removedCount} message${removedCount === 1 ? '' : 's'} removed due to size limit]`,
141
143
  })
142
144
  }
@@ -224,7 +226,7 @@ export const createInstrumentationMiddleware = (
224
226
  }
225
227
  : {}),
226
228
  }
227
- sendEventToPosthog({
229
+ await sendEventToPosthog({
228
230
  client: phClient,
229
231
  distinctId: options.posthogDistinctId,
230
232
  traceId: options.posthogTraceId,
@@ -241,13 +243,13 @@ export const createInstrumentationMiddleware = (
241
243
  outputTokens: result.usage.completionTokens,
242
244
  ...additionalTokenValues,
243
245
  },
244
- fullDebug: options.fullDebug,
246
+ captureImmediate: options.posthogCaptureImmediate,
245
247
  })
246
248
 
247
249
  return result
248
250
  } catch (error: any) {
249
251
  const modelId = model.modelId
250
- sendEventToPosthog({
252
+ await sendEventToPosthog({
251
253
  client: phClient,
252
254
  distinctId: options.posthogDistinctId,
253
255
  traceId: options.posthogTraceId,
@@ -265,7 +267,7 @@ export const createInstrumentationMiddleware = (
265
267
  },
266
268
  isError: true,
267
269
  error: truncate(JSON.stringify(error)),
268
- fullDebug: options.fullDebug,
270
+ captureImmediate: options.posthogCaptureImmediate,
269
271
  })
270
272
  throw error
271
273
  }
@@ -317,9 +319,9 @@ export const createInstrumentationMiddleware = (
317
319
  controller.enqueue(chunk)
318
320
  },
319
321
 
320
- flush() {
322
+ flush: async () => {
321
323
  const latency = (Date.now() - startTime) / 1000
322
- sendEventToPosthog({
324
+ await sendEventToPosthog({
323
325
  client: phClient,
324
326
  distinctId: options.posthogDistinctId,
325
327
  traceId: options.posthogTraceId,
@@ -332,7 +334,7 @@ export const createInstrumentationMiddleware = (
332
334
  params: mergedParams as any,
333
335
  httpStatus: 200,
334
336
  usage,
335
- fullDebug: options.fullDebug,
337
+ captureImmediate: options.posthogCaptureImmediate,
336
338
  })
337
339
  },
338
340
  })
@@ -342,7 +344,7 @@ export const createInstrumentationMiddleware = (
342
344
  ...rest,
343
345
  }
344
346
  } catch (error: any) {
345
- sendEventToPosthog({
347
+ await sendEventToPosthog({
346
348
  client: phClient,
347
349
  distinctId: options.posthogDistinctId,
348
350
  traceId: options.posthogTraceId,
@@ -360,7 +362,7 @@ export const createInstrumentationMiddleware = (
360
362
  },
361
363
  isError: true,
362
364
  error: truncate(JSON.stringify(error)),
363
- fullDebug: options.fullDebug,
365
+ captureImmediate: options.posthogCaptureImmediate,
364
366
  })
365
367
  throw error
366
368
  }
@@ -1,19 +1,65 @@
1
1
  import { PostHog } from 'posthog-node'
2
2
  import PostHogOpenAI from '../src/openai'
3
+ import openaiModule from 'openai'
4
+
5
+ let mockOpenAiChatResponse: any = {}
6
+ let mockOpenAiEmbeddingResponse: any = {}
3
7
 
4
8
  jest.mock('posthog-node', () => {
5
9
  return {
6
10
  PostHog: jest.fn().mockImplementation(() => {
7
11
  return {
8
12
  capture: jest.fn(),
13
+ captureImmediate: jest.fn(),
9
14
  privacyMode: false,
10
15
  }
11
16
  }),
12
17
  }
13
18
  })
14
19
 
15
- let mockOpenAiChatResponse: any = {}
16
- let mockOpenAiEmbeddingResponse: any = {}
20
+ jest.mock('openai', () => {
21
+ // Mock Completions class – `create` is declared on the prototype so that
22
+ // subclasses can safely `super.create(...)` without it being shadowed by an
23
+ // instance field (which would overwrite the subclass implementation).
24
+ class MockCompletions {
25
+ constructor() {}
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ create(..._args: any[]): any {
28
+ /* will be stubbed in beforeEach */
29
+ return undefined
30
+ }
31
+ }
32
+
33
+ // Mock Chat class
34
+ class MockChat {
35
+ constructor() {}
36
+ static Completions = MockCompletions
37
+ }
38
+
39
+ // Mock OpenAI class
40
+ class MockOpenAI {
41
+ chat: any
42
+ embeddings: any
43
+ constructor() {
44
+ this.chat = {
45
+ completions: {
46
+ create: jest.fn(),
47
+ },
48
+ }
49
+ this.embeddings = {
50
+ create: jest.fn(),
51
+ }
52
+ }
53
+ static Chat = MockChat
54
+ }
55
+
56
+ return {
57
+ __esModule: true,
58
+ default: MockOpenAI,
59
+ OpenAI: MockOpenAI,
60
+ Chat: MockChat,
61
+ }
62
+ })
17
63
 
18
64
  describe('PostHogOpenAI - Jest test suite', () => {
19
65
  let mockPostHogClient: PostHog
@@ -79,6 +125,9 @@ describe('PostHogOpenAI - Jest test suite', () => {
79
125
  total_tokens: 10,
80
126
  },
81
127
  }
128
+
129
+ const ChatMock: any = openaiModule.Chat
130
+ ;(ChatMock.Completions as any).prototype.create = jest.fn().mockResolvedValue(mockOpenAiChatResponse)
82
131
  })
83
132
 
84
133
  // Wrap each test with conditional skip
@@ -261,4 +310,18 @@ describe('PostHogOpenAI - Jest test suite', () => {
261
310
  expect(properties['$ai_reasoning_tokens']).toBe(15)
262
311
  expect(properties['$ai_cache_read_input_tokens']).toBe(5)
263
312
  })
313
+
314
+ // New test: ensure captureImmediate is used when flag is set
315
+ conditionalTest('captureImmediate flag', async () => {
316
+ await client.chat.completions.create({
317
+ model: 'gpt-4',
318
+ messages: [{ role: 'user', content: 'Hello' }],
319
+ posthogDistinctId: 'test-id',
320
+ posthogCaptureImmediate: true,
321
+ })
322
+
323
+ // captureImmediate should be called once, and capture should not be called
324
+ expect(mockPostHogClient.captureImmediate).toHaveBeenCalledTimes(1)
325
+ expect(mockPostHogClient.capture).toHaveBeenCalledTimes(0)
326
+ })
264
327
  })
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "extends": "../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "incremental": false,
5
- "types": ["node"],
5
+ "types": ["node", "jest"],
6
6
  "typeRoots": ["./node_modules/@types", "../node_modules/@types"],
7
7
  "moduleResolution": "node",
8
8
  "skipLibCheck": true,