@posthog/ai 4.2.0 → 4.3.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.
@@ -76,6 +76,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
76
76
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
77
77
  posthogPrivacyMode = false,
78
78
  posthogGroups,
79
+ posthogCaptureImmediate,
79
80
  ...openAIParams
80
81
  } = body
81
82
 
@@ -115,7 +116,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
115
116
  }
116
117
 
117
118
  const latency = (Date.now() - startTime) / 1000
118
- sendEventToPosthog({
119
+ await sendEventToPosthog({
119
120
  client: this.phClient,
120
121
  distinctId: posthogDistinctId ?? traceId,
121
122
  traceId,
@@ -128,9 +129,10 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
128
129
  params: body,
129
130
  httpStatus: 200,
130
131
  usage,
132
+ captureImmediate: posthogCaptureImmediate,
131
133
  })
132
134
  } catch (error: any) {
133
- sendEventToPosthog({
135
+ await sendEventToPosthog({
134
136
  client: this.phClient,
135
137
  distinctId: posthogDistinctId ?? traceId,
136
138
  traceId,
@@ -145,6 +147,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
145
147
  usage: { inputTokens: 0, outputTokens: 0 },
146
148
  isError: true,
147
149
  error: JSON.stringify(error),
150
+ captureImmediate: posthogCaptureImmediate,
148
151
  })
149
152
  }
150
153
  })()
@@ -156,10 +159,10 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
156
159
  }) as APIPromise<Stream<ChatCompletionChunk>>
157
160
  } else {
158
161
  const wrappedPromise = parentPromise.then(
159
- (result) => {
162
+ async (result) => {
160
163
  if ('choices' in result) {
161
164
  const latency = (Date.now() - startTime) / 1000
162
- sendEventToPosthog({
165
+ await sendEventToPosthog({
163
166
  client: this.phClient,
164
167
  distinctId: posthogDistinctId ?? traceId,
165
168
  traceId,
@@ -177,12 +180,13 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
177
180
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
178
181
  cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
179
182
  },
183
+ captureImmediate: posthogCaptureImmediate,
180
184
  })
181
185
  }
182
186
  return result
183
187
  },
184
- (error: any) => {
185
- sendEventToPosthog({
188
+ async (error: any) => {
189
+ await sendEventToPosthog({
186
190
  client: this.phClient,
187
191
  distinctId: posthogDistinctId ?? traceId,
188
192
  traceId,
@@ -200,6 +204,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
200
204
  },
201
205
  isError: true,
202
206
  error: JSON.stringify(error),
207
+ captureImmediate: posthogCaptureImmediate,
203
208
  })
204
209
  throw error
205
210
  }
package/src/utils.ts CHANGED
@@ -19,7 +19,7 @@ export interface MonitoringParams {
19
19
  posthogModelOverride?: string
20
20
  posthogProviderOverride?: string
21
21
  posthogCostOverride?: CostOverride
22
- fullDebug?: boolean
22
+ posthogCaptureImmediate?: boolean
23
23
  }
24
24
 
25
25
  export interface CostOverride {
@@ -149,7 +149,7 @@ export type SendEventToPosthogParams = {
149
149
  isError?: boolean
150
150
  error?: string
151
151
  tools?: any
152
- fullDebug?: boolean
152
+ captureImmediate?: boolean
153
153
  }
154
154
 
155
155
  function sanitizeValues(obj: any): any {
@@ -167,7 +167,7 @@ function sanitizeValues(obj: any): any {
167
167
  return jsonSafe
168
168
  }
169
169
 
170
- export const sendEventToPosthog = ({
170
+ export const sendEventToPosthog = async ({
171
171
  client,
172
172
  distinctId,
173
173
  traceId,
@@ -183,68 +183,69 @@ export const sendEventToPosthog = ({
183
183
  isError = false,
184
184
  error,
185
185
  tools,
186
- fullDebug = false,
187
- }: SendEventToPosthogParams): void => {
188
- if (client.capture) {
189
- // sanitize input and output for UTF-8 validity
190
- const safeInput = sanitizeValues(input)
191
- const safeOutput = sanitizeValues(output)
192
- const safeError = sanitizeValues(error)
193
-
194
- let errorData = {}
195
- if (isError) {
196
- errorData = {
197
- $ai_is_error: true,
198
- $ai_error: safeError,
199
- }
186
+ captureImmediate = false,
187
+ }: SendEventToPosthogParams): Promise<void> => {
188
+ if (!client.capture) return Promise.resolve()
189
+ // sanitize input and output for UTF-8 validity
190
+ const safeInput = sanitizeValues(input)
191
+ const safeOutput = sanitizeValues(output)
192
+ const safeError = sanitizeValues(error)
193
+
194
+ let errorData = {}
195
+ if (isError) {
196
+ errorData = {
197
+ $ai_is_error: true,
198
+ $ai_error: safeError,
200
199
  }
201
- let costOverrideData = {}
202
- if (params.posthogCostOverride) {
203
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0)
204
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0)
205
- costOverrideData = {
206
- $ai_input_cost_usd: inputCostUSD,
207
- $ai_output_cost_usd: outputCostUSD,
208
- $ai_total_cost_usd: inputCostUSD + outputCostUSD,
209
- }
200
+ }
201
+ let costOverrideData = {}
202
+ if (params.posthogCostOverride) {
203
+ const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0)
204
+ const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0)
205
+ costOverrideData = {
206
+ $ai_input_cost_usd: inputCostUSD,
207
+ $ai_output_cost_usd: outputCostUSD,
208
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD,
210
209
  }
210
+ }
211
211
 
212
- const additionalTokenValues = {
213
- ...(usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {}),
214
- ...(usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {}),
215
- ...(usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {}),
216
- }
212
+ const additionalTokenValues = {
213
+ ...(usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {}),
214
+ ...(usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {}),
215
+ ...(usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {}),
216
+ }
217
217
 
218
- const properties = {
219
- $ai_provider: params.posthogProviderOverride ?? provider,
220
- $ai_model: params.posthogModelOverride ?? model,
221
- $ai_model_parameters: getModelParams(params),
222
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
223
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
224
- $ai_http_status: httpStatus,
225
- $ai_input_tokens: usage.inputTokens ?? 0,
226
- $ai_output_tokens: usage.outputTokens ?? 0,
227
- ...additionalTokenValues,
228
- $ai_latency: latency,
229
- $ai_trace_id: traceId,
230
- $ai_base_url: baseURL,
231
- ...params.posthogProperties,
232
- ...(distinctId ? {} : { $process_person_profile: false }),
233
- ...(tools ? { $ai_tools: tools } : {}),
234
- ...errorData,
235
- ...costOverrideData,
236
- }
218
+ const properties = {
219
+ $ai_provider: params.posthogProviderOverride ?? provider,
220
+ $ai_model: params.posthogModelOverride ?? model,
221
+ $ai_model_parameters: getModelParams(params),
222
+ $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
223
+ $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
224
+ $ai_http_status: httpStatus,
225
+ $ai_input_tokens: usage.inputTokens ?? 0,
226
+ $ai_output_tokens: usage.outputTokens ?? 0,
227
+ ...additionalTokenValues,
228
+ $ai_latency: latency,
229
+ $ai_trace_id: traceId,
230
+ $ai_base_url: baseURL,
231
+ ...params.posthogProperties,
232
+ ...(distinctId ? {} : { $process_person_profile: false }),
233
+ ...(tools ? { $ai_tools: tools } : {}),
234
+ ...errorData,
235
+ ...costOverrideData,
236
+ }
237
237
 
238
- if (fullDebug) {
239
- // @ts-ignore
240
- console.log('Sending event to PostHog', properties)
241
- }
238
+ const event = {
239
+ distinctId: distinctId ?? traceId,
240
+ event: '$ai_generation',
241
+ properties,
242
+ groups: params.posthogGroups,
243
+ }
242
244
 
243
- client.capture({
244
- distinctId: distinctId ?? traceId,
245
- event: '$ai_generation',
246
- properties,
247
- groups: params.posthogGroups,
248
- })
245
+ if (captureImmediate) {
246
+ // await capture promise to send single event in serverless environments
247
+ await client.captureImmediate(event)
248
+ } else {
249
+ client.capture(event)
249
250
  }
250
251
  }
@@ -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 {
@@ -127,15 +127,23 @@ const mapVercelPrompt = (prompt: LanguageModelV1Prompt): PostHogInput[] => {
127
127
  try {
128
128
  // Trim the inputs array until its JSON size fits within MAX_OUTPUT_SIZE
129
129
  let serialized = JSON.stringify(inputs)
130
- while (Buffer.byteLength(serialized, 'utf8') > MAX_OUTPUT_SIZE && inputs.length > 0) {
131
- // Remove oldest message
130
+ let removedCount = 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
- // add blank message to beginning of array
134
- inputs.unshift({ role: 'assistant', content: '[removed message due to size limit]' })
135
+ removedCount++
135
136
  serialized = JSON.stringify(inputs)
136
137
  }
138
+ if (removedCount > 0) {
139
+ // Add one placeholder to indicate how many were removed
140
+ inputs.unshift({
141
+ role: 'posthog',
142
+ content: `[${removedCount} message${removedCount === 1 ? '' : 's'} removed due to size limit]`,
143
+ })
144
+ }
137
145
  } catch (error) {
138
- console.error('Error stringifying inputs')
146
+ console.error('Error stringifying inputs', error)
139
147
  return [{ role: 'posthog', content: 'An error occurred while processing your request. Please try again.' }]
140
148
  }
141
149
  return inputs
@@ -218,7 +226,7 @@ export const createInstrumentationMiddleware = (
218
226
  }
219
227
  : {}),
220
228
  }
221
- sendEventToPosthog({
229
+ await sendEventToPosthog({
222
230
  client: phClient,
223
231
  distinctId: options.posthogDistinctId,
224
232
  traceId: options.posthogTraceId,
@@ -235,13 +243,13 @@ export const createInstrumentationMiddleware = (
235
243
  outputTokens: result.usage.completionTokens,
236
244
  ...additionalTokenValues,
237
245
  },
238
- fullDebug: options.fullDebug,
246
+ captureImmediate: options.posthogCaptureImmediate,
239
247
  })
240
248
 
241
249
  return result
242
250
  } catch (error: any) {
243
251
  const modelId = model.modelId
244
- sendEventToPosthog({
252
+ await sendEventToPosthog({
245
253
  client: phClient,
246
254
  distinctId: options.posthogDistinctId,
247
255
  traceId: options.posthogTraceId,
@@ -259,7 +267,7 @@ export const createInstrumentationMiddleware = (
259
267
  },
260
268
  isError: true,
261
269
  error: truncate(JSON.stringify(error)),
262
- fullDebug: options.fullDebug,
270
+ captureImmediate: options.posthogCaptureImmediate,
263
271
  })
264
272
  throw error
265
273
  }
@@ -311,9 +319,9 @@ export const createInstrumentationMiddleware = (
311
319
  controller.enqueue(chunk)
312
320
  },
313
321
 
314
- flush() {
322
+ flush: async () => {
315
323
  const latency = (Date.now() - startTime) / 1000
316
- sendEventToPosthog({
324
+ await sendEventToPosthog({
317
325
  client: phClient,
318
326
  distinctId: options.posthogDistinctId,
319
327
  traceId: options.posthogTraceId,
@@ -326,7 +334,7 @@ export const createInstrumentationMiddleware = (
326
334
  params: mergedParams as any,
327
335
  httpStatus: 200,
328
336
  usage,
329
- fullDebug: options.fullDebug,
337
+ captureImmediate: options.posthogCaptureImmediate,
330
338
  })
331
339
  },
332
340
  })
@@ -336,7 +344,7 @@ export const createInstrumentationMiddleware = (
336
344
  ...rest,
337
345
  }
338
346
  } catch (error: any) {
339
- sendEventToPosthog({
347
+ await sendEventToPosthog({
340
348
  client: phClient,
341
349
  distinctId: options.posthogDistinctId,
342
350
  traceId: options.posthogTraceId,
@@ -354,7 +362,7 @@ export const createInstrumentationMiddleware = (
354
362
  },
355
363
  isError: true,
356
364
  error: truncate(JSON.stringify(error)),
357
- fullDebug: options.fullDebug,
365
+ captureImmediate: options.posthogCaptureImmediate,
358
366
  })
359
367
  throw error
360
368
  }
@@ -6,6 +6,7 @@ jest.mock('posthog-node', () => {
6
6
  PostHog: jest.fn().mockImplementation(() => {
7
7
  return {
8
8
  capture: jest.fn(),
9
+ captureImmediate: jest.fn(),
9
10
  privacyMode: false,
10
11
  }
11
12
  }),
@@ -261,4 +262,18 @@ describe('PostHogOpenAI - Jest test suite', () => {
261
262
  expect(properties['$ai_reasoning_tokens']).toBe(15)
262
263
  expect(properties['$ai_cache_read_input_tokens']).toBe(5)
263
264
  })
265
+
266
+ // New test: ensure captureImmediate is used when flag is set
267
+ conditionalTest('captureImmediate flag', async () => {
268
+ await client.chat.completions.create({
269
+ model: 'gpt-4',
270
+ messages: [{ role: 'user', content: 'Hello' }],
271
+ posthogDistinctId: 'test-id',
272
+ posthogCaptureImmediate: true,
273
+ })
274
+
275
+ // captureImmediate should be called once, and capture should not be called
276
+ expect(mockPostHogClient.captureImmediate).toHaveBeenCalledTimes(1)
277
+ expect(mockPostHogClient.capture).toHaveBeenCalledTimes(0)
278
+ })
264
279
  })
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,