@posthog/core 1.27.9 → 1.28.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.
Files changed (55) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/logs/index.d.ts +94 -4
  4. package/dist/logs/index.d.ts.map +1 -1
  5. package/dist/logs/index.js +149 -4
  6. package/dist/logs/index.mjs +150 -5
  7. package/dist/logs/logs-utils.d.ts +2 -1
  8. package/dist/logs/logs-utils.d.ts.map +1 -1
  9. package/dist/logs/types.d.ts +140 -8
  10. package/dist/logs/types.d.ts.map +1 -1
  11. package/dist/posthog-core-stateless.d.ts +34 -0
  12. package/dist/posthog-core-stateless.d.ts.map +1 -1
  13. package/dist/posthog-core-stateless.js +46 -0
  14. package/dist/posthog-core-stateless.mjs +46 -0
  15. package/dist/posthog-core.d.ts.map +1 -1
  16. package/dist/posthog-core.js +2 -0
  17. package/dist/posthog-core.mjs +2 -0
  18. package/dist/surveys/events.d.ts +22 -0
  19. package/dist/surveys/events.d.ts.map +1 -0
  20. package/dist/surveys/events.js +95 -0
  21. package/dist/surveys/events.mjs +43 -0
  22. package/dist/surveys/index.d.ts +4 -0
  23. package/dist/surveys/index.d.ts.map +1 -0
  24. package/dist/surveys/index.js +83 -0
  25. package/dist/surveys/index.mjs +4 -0
  26. package/dist/surveys/translations.d.ts +38 -0
  27. package/dist/surveys/translations.d.ts.map +1 -0
  28. package/dist/surveys/translations.js +207 -0
  29. package/dist/surveys/translations.mjs +158 -0
  30. package/dist/testing/test-utils.d.ts.map +1 -1
  31. package/dist/testing/test-utils.js +1 -0
  32. package/dist/testing/test-utils.mjs +1 -0
  33. package/dist/types.d.ts +31 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/utils/logger.d.ts +1 -1
  36. package/dist/utils/logger.d.ts.map +1 -1
  37. package/dist/utils/logger.js +3 -0
  38. package/dist/utils/logger.mjs +3 -0
  39. package/package.json +26 -2
  40. package/src/index.ts +12 -1
  41. package/src/logs/index.spec.ts +891 -17
  42. package/src/logs/index.ts +341 -11
  43. package/src/logs/logs-utils.spec.ts +2 -1
  44. package/src/logs/logs-utils.ts +1 -1
  45. package/src/logs/types.ts +150 -25
  46. package/src/posthog-core-stateless.ts +80 -0
  47. package/src/posthog-core.ts +6 -0
  48. package/src/surveys/events.spec.ts +52 -0
  49. package/src/surveys/events.ts +80 -0
  50. package/src/surveys/index.ts +18 -0
  51. package/src/surveys/translations.spec.ts +205 -0
  52. package/src/surveys/translations.ts +244 -0
  53. package/src/testing/test-utils.ts +1 -0
  54. package/src/types.ts +38 -2
  55. package/src/utils/logger.ts +6 -2
package/src/logs/index.ts CHANGED
@@ -1,26 +1,60 @@
1
- import type { LogSdkContext } from '@posthog/types'
2
- import { buildOtlpLogRecord } from './logs-utils'
1
+ import type { LogAttributeValue } from '@posthog/types'
2
+ import { buildOtlpLogRecord, buildOtlpLogsPayload } from './logs-utils'
3
3
  import { Logger, PostHogPersistedProperty } from '../types'
4
4
  import type { PostHogCoreStateless } from '../posthog-core-stateless'
5
- import type { BufferedLogEntry, CaptureLogOptions, ResolvedPostHogLogsConfig } from './types'
5
+ import { isArray, safeSetTimeout } from '../utils'
6
+ import type {
7
+ BeforeSendLogFn,
8
+ BufferedLogEntry,
9
+ CaptureLogOptions,
10
+ LogSdkContext,
11
+ ResolvedPostHogLogsConfig,
12
+ } from './types'
6
13
 
7
14
  export class PostHogLogs {
8
- private _localEnabled: boolean
9
15
  private _maxBufferSize: number
16
+ private _flushIntervalMs: number
17
+ // Mutable — halved on 413 to shrink the next POST, and ramped back up by
18
+ // one record after each successful send so a one-off oversized payload
19
+ // (e.g. a giant stack trace) doesn't permanently degrade throughput.
20
+ private _maxBatchRecordsPerPost: number
21
+ private _flushTimer?: ReturnType<typeof safeSetTimeout>
22
+ // Serializes concurrent flushes — the second caller awaits the first rather
23
+ // than racing it and double-sending the same head-of-queue records.
24
+ private _flushPromise: Promise<void> | null = null
25
+
26
+ // Fixed-window rate cap. Tumbling (not sliding) for cheap arithmetic on the
27
+ // hot path. Window rolls the first time `captureLog` fires after the window
28
+ // expires — no background timer needed. `_droppedWarned` keeps the log noise
29
+ // to one line per window regardless of how many records got dropped.
30
+ private _rateCapWindowMs: number
31
+ private _maxLogsPerInterval?: number
32
+ private _intervalWindowStart = 0
33
+ private _intervalLogCount = 0
34
+ private _droppedWarned = false
10
35
 
11
36
  constructor(
12
37
  private readonly _instance: PostHogCoreStateless,
13
38
  private readonly _config: ResolvedPostHogLogsConfig,
14
39
  private readonly _logger: Logger,
15
40
  private readonly _getContext: () => LogSdkContext,
16
- private readonly _onReady: (fn: () => void) => void
41
+ private readonly _onReady: (fn: () => void) => void,
42
+ // Waits for the logs-storage persist to hit disk. Called between batches
43
+ // so a crash after a successful HTTP send but before the queue-advance
44
+ // reaches disk can't cause duplicate records on next startup. SDKs with
45
+ // synchronous storage (or no async persist layer) can pass a no-op. RN
46
+ // wires this to its dedicated `_logsStorage.waitForPersist()`.
47
+ private readonly _waitForStoragePersist: () => Promise<void> = () => Promise.resolve()
17
48
  ) {
18
- this._localEnabled = _config.enabled !== false
19
49
  this._maxBufferSize = _config.maxBufferSize
50
+ this._flushIntervalMs = _config.flushIntervalMs
51
+ this._maxBatchRecordsPerPost = _config.maxBatchRecordsPerPost
52
+ this._rateCapWindowMs = _config.rateCapWindowMs
53
+ this._maxLogsPerInterval = _config.maxLogsPerInterval
20
54
  }
21
55
 
22
56
  captureLog(options: CaptureLogOptions): void {
23
- if (!this._localEnabled) {
57
+ if (this._instance.isDisabled) {
24
58
  return
25
59
  }
26
60
  if (this._instance.optedOut) {
@@ -30,19 +64,230 @@ export class PostHogLogs {
30
64
  return
31
65
  }
32
66
 
67
+ // Ordering: beforeSend → rate cap → OTLP build. beforeSend runs first so
68
+ // user-dropped records don't consume the per-interval budget.
69
+ const filtered = this._runBeforeSend(options)
70
+ if (filtered === null) {
71
+ return
72
+ }
73
+ // beforeSend could return a record with empty body — treat as drop.
74
+ if (!filtered.body) {
75
+ return
76
+ }
77
+
78
+ if (!this._checkRateLimit()) {
79
+ return
80
+ }
81
+
33
82
  // Build before deferring so attributes reflect state at capture time, not
34
83
  // at drain time (identity/session changes between capture and drain must
35
84
  // not corrupt recorded attributes).
36
- const record = buildOtlpLogRecord(options, this._getContext())
85
+ const record = buildOtlpLogRecord(filtered, this._getContext())
37
86
  const entry: BufferedLogEntry = { record }
38
87
 
39
88
  this._onReady(() => this._enqueue(entry))
40
89
  }
41
90
 
91
+ /**
92
+ * Runs the configured `beforeSend` hook(s) on a capture record:
93
+ * - single fn OR array of fns (chain, left-to-right)
94
+ * - returning `null` drops the record (logged at info)
95
+ * - a thrown error is logged and the chain *continues* with the previous
96
+ * result — a buggy user filter must never crash the caller's
97
+ * `captureLog()` call
98
+ */
99
+ private _runBeforeSend(options: CaptureLogOptions): CaptureLogOptions | null {
100
+ const beforeSend = this._config.beforeSend
101
+ if (!beforeSend) {
102
+ return options
103
+ }
104
+ const fns = isArray(beforeSend) ? beforeSend : [beforeSend]
105
+ let result: CaptureLogOptions = options
106
+ for (const fn of fns) {
107
+ try {
108
+ const next = fn(result)
109
+ if (!next) {
110
+ this._logger.info(`Log was rejected in beforeSend function`)
111
+ return null
112
+ }
113
+ result = next
114
+ } catch (e) {
115
+ // Swallow the throw — the chain continues with `result` unchanged so
116
+ // a buggy filter degrades to a no-op rather than crashing the app.
117
+ this._logger.error(`Error in beforeSend function for log:`, e)
118
+ }
119
+ }
120
+ return result
121
+ }
122
+
123
+ /**
124
+ * Returns `true` if this capture fits within the current rate-cap window,
125
+ * `false` if it should be dropped.
126
+ *
127
+ * Fixed (tumbling) window: the counter resets the first time `captureLog`
128
+ * fires after `rateCapWindowMs` has elapsed — no timer needed.
129
+ * `maxLogsPerInterval === undefined` means unbounded.
130
+ *
131
+ * Wall-clock safety: if `Date.now()` jumps backward (manual device-clock
132
+ * change, big NTP correction), `elapsed` goes negative. We treat that the
133
+ * same as "window expired" and reset — otherwise the rate cap would be
134
+ * stuck until the clock caught up to the old window start, potentially
135
+ * dropping logs for hours.
136
+ *
137
+ * Pre-init note: the counter increments here, before `_onReady` defers
138
+ * `_enqueue` to the init promise. If init resolves slowly and the user is
139
+ * later opted out, the counter has already consumed budget for records
140
+ * that won't enqueue. Cosmetic — no record is "lost" beyond what's
141
+ * already gated, and the window rolls on its own.
142
+ */
143
+ private _checkRateLimit(): boolean {
144
+ if (this._maxLogsPerInterval === undefined) {
145
+ return true
146
+ }
147
+ const now = Date.now()
148
+ const elapsed = now - this._intervalWindowStart
149
+ if (elapsed >= this._rateCapWindowMs || elapsed < 0) {
150
+ this._intervalWindowStart = now
151
+ this._intervalLogCount = 0
152
+ this._droppedWarned = false
153
+ }
154
+ if (this._intervalLogCount >= this._maxLogsPerInterval) {
155
+ if (!this._droppedWarned) {
156
+ this._logger.warn(
157
+ `captureLog dropping logs: exceeded ${this._maxLogsPerInterval} logs per ${this._rateCapWindowMs}ms`
158
+ )
159
+ this._droppedWarned = true
160
+ }
161
+ return false
162
+ }
163
+ this._intervalLogCount++
164
+ return true
165
+ }
166
+
167
+ /**
168
+ * Drains `LogsQueue` in `maxBatchRecordsPerPost` slices, POSTing each as an
169
+ * OTLP payload.
170
+ * - Network error → keep items in queue, re-throw (caller retries later)
171
+ * - 413 → halve batch size, retry same records (do not advance)
172
+ * - Any other error → drop the batch (avoid infinite loop on malformed data),
173
+ * re-throw so callers can log/report
174
+ * Concurrent calls are serialized through `_flushPromise` so records at the
175
+ * head of the queue can't be sent twice.
176
+ */
177
+ async flush(): Promise<void> {
178
+ if (this._instance.isDisabled) {
179
+ return
180
+ }
181
+ if (this._flushPromise) {
182
+ return this._flushPromise
183
+ }
184
+ this._flushPromise = this._flushInner().finally(() => {
185
+ this._flushPromise = null
186
+ })
187
+ return this._flushPromise
188
+ }
189
+
190
+ private async _flushInner(): Promise<void> {
191
+ this._clearFlushTimer()
192
+
193
+ let queue = this._instance.getPersistedProperty<BufferedLogEntry[]>(PostHogPersistedProperty.LogsQueue) ?? []
194
+ if (queue.length === 0) {
195
+ return
196
+ }
197
+
198
+ const originalQueueLength = queue.length
199
+ let sentCount = 0
200
+
201
+ while (queue.length > 0 && sentCount < originalQueueLength) {
202
+ const batchSize = Math.min(queue.length, this._maxBatchRecordsPerPost)
203
+ const batch = queue.slice(0, batchSize)
204
+ const records = batch.map((e) => e.record)
205
+
206
+ const payload = buildOtlpLogsPayload(
207
+ records,
208
+ this._buildResourceAttributes(),
209
+ this._instance.getLibraryId(),
210
+ this._instance.getLibraryVersion()
211
+ )
212
+
213
+ const outcome = await this._instance._sendLogsBatch(payload)
214
+
215
+ if (outcome.kind === 'too-large' && batch.length > 1) {
216
+ this._maxBatchRecordsPerPost = Math.max(1, Math.floor(batch.length / 2))
217
+ this._logger.warn(
218
+ `Received 413 when sending logs batch of size ${batch.length}, reducing batch size to ${this._maxBatchRecordsPerPost}`
219
+ )
220
+ // Don't advance the queue — retry the same records with the smaller cap.
221
+ continue
222
+ }
223
+
224
+ if (outcome.kind === 'retry-later') {
225
+ // Network error: keep records in the queue for the next flush cycle
226
+ // and surface the error so the caller can log/react.
227
+ throw outcome.error
228
+ }
229
+
230
+ // ok | fatal | too-large-with-batch-of-1 → records are leaving the
231
+ // queue. 'fatal' and size-1 413s are dropped so we don't spin on the
232
+ // same record forever. Surface the size-1 413 explicitly so a single
233
+ // oversized record (e.g. a giant body field) is visible in logs
234
+ // instead of silently disappearing.
235
+ if (outcome.kind === 'too-large') {
236
+ this._logger.warn(
237
+ 'Dropping a single log record after 413 with batch size 1 — the record is larger than the server cap and cannot be split further.'
238
+ )
239
+ } else if (outcome.kind === 'ok' && this._maxBatchRecordsPerPost < this._config.maxBatchRecordsPerPost) {
240
+ // Linear recovery: each healthy send pushes the cap back up by 1
241
+ // toward the configured maximum. Linear (not doubling) so we don't
242
+ // immediately retrigger a 413 if the previous shrink was justified.
243
+ this._maxBatchRecordsPerPost = Math.min(this._config.maxBatchRecordsPerPost, this._maxBatchRecordsPerPost + 1)
244
+ }
245
+ await this._persistQueueAdvance(batch.length)
246
+ queue = this._instance.getPersistedProperty<BufferedLogEntry[]>(PostHogPersistedProperty.LogsQueue) ?? []
247
+ sentCount += batch.length
248
+
249
+ if (outcome.kind === 'fatal') {
250
+ throw outcome.error
251
+ }
252
+ }
253
+ }
254
+
255
+ private async _persistQueueAdvance(consumed: number): Promise<void> {
256
+ // Re-read the queue in case captures landed mid-flush, then drop the head.
257
+ const refreshed = this._instance.getPersistedProperty<BufferedLogEntry[]>(PostHogPersistedProperty.LogsQueue) ?? []
258
+ this._instance.setPersistedProperty(PostHogPersistedProperty.LogsQueue, refreshed.slice(consumed))
259
+ // Wait for the advance to hit disk before the next batch sends, matching
260
+ // events' `flushStorage()` contract. Prevents duplicates if the app crashes
261
+ // after the HTTP success but before the queue-advance persists.
262
+ await this._waitForStoragePersist()
263
+ }
264
+
265
+ /**
266
+ * OTLP resource attributes for every batch.
267
+ *
268
+ * Layout: user `resourceAttributes` spread first, then SDK-controlled
269
+ * keys layered on top so users cannot accidentally clobber them. Most logs
270
+ * backends index on `service.name` and `telemetry.sdk.*` for routing,
271
+ * SDK-version dashboards, and bug-correlation; letting a stray user key
272
+ * overwrite them silently breaks ingestion attribution. The dedicated
273
+ * `serviceName` / `environment` / `serviceVersion` config fields are the
274
+ * supported way to override `service.name` / `deployment.environment` /
275
+ * `service.version`.
276
+ */
277
+ private _buildResourceAttributes(): Record<string, LogAttributeValue> {
278
+ return {
279
+ ...this._config.resourceAttributes,
280
+ 'service.name': this._config.serviceName || 'unknown_service',
281
+ ...(this._config.environment && { 'deployment.environment': this._config.environment }),
282
+ ...(this._config.serviceVersion && { 'service.version': this._config.serviceVersion }),
283
+ 'telemetry.sdk.name': this._instance.getLibraryId(),
284
+ 'telemetry.sdk.version': this._instance.getLibraryVersion(),
285
+ }
286
+ }
287
+
42
288
  private _enqueue(entry: BufferedLogEntry): void {
43
- // Re-check: optedOut can flip between captureLog and here preload may
44
- // have hydrated the real persisted value, or optIn/optOut may have fired
45
- // while this fn was deferred.
289
+ // Re-check optedOut: preload may have hydrated the real persisted value,
290
+ // or optIn/optOut may have fired while this fn was deferred.
46
291
  if (this._instance.optedOut) {
47
292
  return
48
293
  }
@@ -54,5 +299,90 @@ export class PostHogLogs {
54
299
  }
55
300
  queue.push(entry)
56
301
  this._instance.setPersistedProperty(PostHogPersistedProperty.LogsQueue, queue)
302
+
303
+ // Threshold trigger: at-capacity means flushing now reclaims space before
304
+ // the next capture has to shift something out.
305
+ if (queue.length >= this._maxBufferSize) {
306
+ this._flushInBackground()
307
+ return
308
+ }
309
+
310
+ // Timer trigger: only arm one timer at a time. A subsequent enqueue within
311
+ // the window shouldn't reschedule — that would keep pushing the flush out.
312
+ if (!this._flushTimer) {
313
+ this._flushTimer = safeSetTimeout(() => {
314
+ this._flushTimer = undefined
315
+ this._flushInBackground()
316
+ }, this._flushIntervalMs)
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Stops the timer-based flush and sends anything still in the queue.
322
+ * Intended for process-teardown paths (RN `_shutdown` override). Swallows
323
+ * errors so a failing final flush can't block the broader shutdown.
324
+ *
325
+ * If `timeoutMs` is provided, the final flush races against that budget so
326
+ * a slow network/storage can't hold up shutdown indefinitely. Without it,
327
+ * flush time is bounded only by `fetchRetryCount * (requestTimeout +
328
+ * fetchRetryDelay)`, which can exceed the caller's shutdown SLA.
329
+ */
330
+ async shutdown(timeoutMs?: number): Promise<void> {
331
+ this._clearFlushTimer()
332
+ const flushPromise = this.flush().catch(() => {
333
+ // Best-effort: a logs-flush failure during shutdown is not actionable
334
+ // and must not prevent the rest of shutdown from running. Errors are
335
+ // still surfaced from the regular `flush()` path in normal operation.
336
+ })
337
+ if (timeoutMs === undefined) {
338
+ await flushPromise
339
+ return
340
+ }
341
+ await Promise.race([flushPromise, new Promise<void>((resolve) => safeSetTimeout(resolve, timeoutMs))])
342
+ }
343
+
344
+ /**
345
+ * Time-bounded flush for transient lifecycle events (e.g. RN
346
+ * foreground→background) that must complete inside an OS-imposed window.
347
+ * Unlike `shutdown`, this leaves the periodic flush timer in place so the
348
+ * pipeline keeps draining if the process is resumed instead of suspended.
349
+ *
350
+ * Errors propagate so the host SDK can route them through its standard
351
+ * lifecycle error handler (e.g. RN's `logFlushError`). If the timer wins
352
+ * the race, a late rejection from the in-flight flush is silenced via a
353
+ * no-op handler attached after the race settles, to avoid noisy
354
+ * unhandled-rejection logs — the next regular flush cycle will retry.
355
+ */
356
+ async flushWithTimeout(timeoutMs: number): Promise<void> {
357
+ let timedOut = false
358
+ const flushPromise = this.flush()
359
+ const timerPromise = new Promise<void>((resolve) =>
360
+ safeSetTimeout(() => {
361
+ timedOut = true
362
+ resolve()
363
+ }, timeoutMs)
364
+ )
365
+ try {
366
+ await Promise.race([flushPromise, timerPromise])
367
+ } finally {
368
+ if (timedOut) {
369
+ // Race lost — flush is still in flight. Attach a no-op rejection
370
+ // handler so a late failure isn't logged as unhandled.
371
+ void flushPromise.catch(() => {})
372
+ }
373
+ }
374
+ }
375
+
376
+ private _flushInBackground(): void {
377
+ void this.flush().catch((err) => {
378
+ this._logger.error('PostHog logs flush failed:', err)
379
+ })
380
+ }
381
+
382
+ private _clearFlushTimer(): void {
383
+ if (this._flushTimer) {
384
+ clearTimeout(this._flushTimer)
385
+ this._flushTimer = undefined
386
+ }
57
387
  }
58
388
  }
@@ -1,4 +1,5 @@
1
- import type { CaptureLogOptions, LogSdkContext, LogSeverityLevel } from '@posthog/types'
1
+ import type { CaptureLogOptions, LogSeverityLevel } from '@posthog/types'
2
+ import type { LogSdkContext } from './types'
2
3
  import {
3
4
  buildOtlpLogRecord,
4
5
  buildOtlpLogsPayload,
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  CaptureLogOptions,
3
3
  LogAttributeValue,
4
- LogSdkContext,
5
4
  LogSeverityLevel,
6
5
  OtlpAnyValue,
7
6
  OtlpKeyValue,
@@ -10,6 +9,7 @@ import type {
10
9
  OtlpSeverityEntry,
11
10
  OtlpSeverityText,
12
11
  } from '@posthog/types'
12
+ import type { LogSdkContext } from './types'
13
13
  import { isArray, isBoolean, isNull, isUndefined } from '../utils'
14
14
 
15
15
  // ============================================================================
package/src/logs/types.ts CHANGED
@@ -11,9 +11,28 @@ export type {
11
11
  OtlpKeyValue,
12
12
  OtlpLogRecord,
13
13
  OtlpLogsPayload,
14
- LogSdkContext,
15
14
  } from '@posthog/types'
16
15
 
16
+ /**
17
+ * SDK-internal context the host SDK passes to `buildOtlpLogRecord` at capture
18
+ * time. Each SDK populates the fields that apply to it: browser fills
19
+ * `currentUrl`, mobile fills `screenName` / `appState`. Missing fields are
20
+ * omitted from the OTLP record (no stray attributes).
21
+ *
22
+ * Internal to `@posthog/core` — customers don't see this in autocomplete.
23
+ */
24
+ export interface LogSdkContext {
25
+ distinctId?: string
26
+ sessionId?: string
27
+ /** Web-only — current page URL */
28
+ currentUrl?: string
29
+ /** Mobile-only — current screen / view name */
30
+ screenName?: string
31
+ /** Mobile-only — app foreground/background state at capture time */
32
+ appState?: 'foreground' | 'background'
33
+ activeFeatureFlags?: string[]
34
+ }
35
+
17
36
  // The public capture-logger interface lives in @posthog/types as `Logger`. Core
18
37
  // also exports a `Logger` (the SDK's internal warn/info/error logger). Alias the
19
38
  // public one to avoid the name collision inside this package.
@@ -22,45 +41,151 @@ export type CaptureLogger = CaptureLoggerType
22
41
 
23
42
  import type { LogAttributeValue, CaptureLogOptions, OtlpLogRecord } from '@posthog/types'
24
43
 
25
- // Wrapper around OtlpLogRecord for queue entries. Parallels events' queue
26
- // item shape (`{ message }`) — future additions like `retryCount` or
27
- // `enqueuedAt` can be added without migrating the queue format.
28
44
  export interface BufferedLogEntry {
29
45
  record: OtlpLogRecord
30
46
  }
31
47
 
32
- // Public configuration for the logs module. Per-SDK defaults diverge (mobile
33
- // cellular radio cost, browser tab suspension, node process lifecycle).
34
- export interface PostHogLogsConfig {
35
- // Master switch. Default: true when a config object is provided.
36
- enabled?: boolean
48
+ /**
49
+ * Pre-send filter. Inspect, mutate, or drop a captured record before it
50
+ * enters the rate-cap or the queue. Return the (possibly transformed) record
51
+ * to keep it; return `null` to drop it.
52
+ *
53
+ * Configure as a single fn or an array. Arrays form a left-to-right chain:
54
+ * each fn receives the previous fn's return value. A `null` from any link
55
+ * short-circuits the chain and drops the record.
56
+ *
57
+ * Runs *before* the rate cap so dropped records don't consume the
58
+ * per-interval budget. Throwing fns are logged and skipped — the chain
59
+ * continues with the previous return value, so a buggy filter degrades to a
60
+ * no-op rather than crashing `captureLog()`.
61
+ *
62
+ * @example Redact secrets from log bodies
63
+ * ```ts
64
+ * logs: {
65
+ * beforeSend: (record) => ({
66
+ * ...record,
67
+ * body: record.body.replace(/api_key=\S+/g, 'api_key=[REDACTED]'),
68
+ * }),
69
+ * }
70
+ * ```
71
+ *
72
+ * @example Drop noisy debug logs in production
73
+ * ```ts
74
+ * logs: {
75
+ * beforeSend: (record) => (record.level === 'debug' ? null : record),
76
+ * }
77
+ * ```
78
+ */
79
+ export type BeforeSendLogFn = (record: CaptureLogOptions) => CaptureLogOptions | null
37
80
 
38
- // Resource attributes
81
+ /**
82
+ * Configuration for the logs feature on `new PostHog(key, { logs: ... })`.
83
+ * All fields are optional; per-SDK defaults apply (mobile vs browser tune
84
+ * differently for cellular cost vs tab-suspension behavior).
85
+ */
86
+ export interface PostHogLogsConfig {
87
+ /**
88
+ * Service name attached to every record as the OTLP `service.name`
89
+ * resource attribute. Used by the Logs UI for filtering / grouping.
90
+ * Default: `'unknown_service'`.
91
+ */
39
92
  serviceName?: string
93
+
94
+ /**
95
+ * Service version attached as OTLP `service.version`. Useful for
96
+ * correlating regressions to specific app releases.
97
+ */
40
98
  serviceVersion?: string
99
+
100
+ /**
101
+ * Deployment environment attached as OTLP `deployment.environment`
102
+ * (e.g. `'production'`, `'staging'`, `'dev'`).
103
+ */
41
104
  environment?: string
105
+
106
+ /**
107
+ * Extra OTLP resource attributes attached to every record. Spread first;
108
+ * SDK-controlled keys (`service.name`, `telemetry.sdk.*`, RN's `os.*`)
109
+ * are layered on top so users cannot accidentally clobber them. Use the
110
+ * dedicated `serviceName` / `environment` / `serviceVersion` fields to
111
+ * override those keys.
112
+ */
42
113
  resourceAttributes?: Record<string, LogAttributeValue>
43
114
 
44
- // Buffering
115
+ /**
116
+ * How often the periodic background flush fires (ms). Records also flush
117
+ * eagerly when the buffer fills, on AppState changes (RN), and on
118
+ * `shutdown()`. Lower values trade battery/bandwidth for fresher data.
119
+ * Default: 10000 (RN) / 3000 (browser).
120
+ */
45
121
  flushIntervalMs?: number
46
- rateCapWindowMs?: number // separate from flushIntervalMs so flush cadence does not move the rate-cap window
122
+
123
+ /**
124
+ * Max records held in memory before the queue evicts the oldest on push
125
+ * (FIFO). Bounds memory footprint and on-disk-queue size. When the buffer
126
+ * hits this size, an immediate flush is triggered to reclaim space; if
127
+ * the flush hasn't completed before the next capture, the oldest record
128
+ * is shifted out. Default: 100.
129
+ */
47
130
  maxBufferSize?: number
48
- maxLogsPerInterval?: number
49
- maxBatchRecordsPerPost?: number // keeps each POST under the 2 MB server cap
50
131
 
51
- // Shutdown — separate budgets because foreground→background and app-terminate
52
- // have different OS-imposed windows.
53
- backgroundFlushBudgetMs?: number
54
- terminationFlushBudgetMs?: number
132
+ /**
133
+ * Max records per outbound POST. Keeps each request under the server's
134
+ * 2 MB cap. On a 413 response, the SDK halves this value, retries the
135
+ * same records, then ramps back up by 1 per healthy send. A 413 on a
136
+ * single-record batch drops the record (it's larger than the server can
137
+ * accept regardless of batch size). Default: 50 (RN) / 100 (browser).
138
+ */
139
+ maxBatchRecordsPerPost?: number
55
140
 
56
- // Filtering. Runs synchronously before the rate cap so beforeSend-dropped
57
- // records do not consume the per-interval budget.
58
- beforeSend?: (record: CaptureLogOptions) => CaptureLogOptions | null
141
+ /**
142
+ * Tumbling-window rate cap. Bounds how many records can be captured
143
+ * within a sliding (technically tumbling) time window. Records exceeding
144
+ * the cap are dropped synchronously at `captureLog()` (they never enter
145
+ * the buffer or consume bandwidth). A single warn line is logged per
146
+ * window when the cap is hit.
147
+ *
148
+ * Defaults are per-SDK; on RN the default is `{ maxLogs: 500, windowMs:
149
+ * 10000 }` (≈50 logs/sec ceiling, tuned for cellular bandwidth).
150
+ *
151
+ * @example Allow brief bursts up to 1000/min
152
+ * ```ts
153
+ * logs: { rateCap: { maxLogs: 1000, windowMs: 60000 } }
154
+ * ```
155
+ *
156
+ * @example Disable the cap entirely (unbounded)
157
+ * ```ts
158
+ * logs: { rateCap: { maxLogs: undefined } }
159
+ * ```
160
+ */
161
+ rateCap?: {
162
+ /**
163
+ * Max records accepted per `windowMs` window. `undefined` = unbounded.
164
+ */
165
+ maxLogs?: number
166
+ /**
167
+ * Window length in ms. Tumbling, not sliding — the counter resets the
168
+ * first time a capture fires after the window expires.
169
+ */
170
+ windowMs?: number
171
+ }
172
+
173
+ /**
174
+ * Pre-send filter. See {@link BeforeSendLogFn} for shape and examples.
175
+ * Configure as a single function or a chain.
176
+ */
177
+ beforeSend?: BeforeSendLogFn | BeforeSendLogFn[]
59
178
  }
60
179
 
61
- // Fields PostHogLogs needs resolved at runtime. Each SDK supplies its own
62
- // defaults (mobile, browser, node have different right answers) and hands the
63
- // filled-in config to the PostHogLogs constructor.
64
- export interface ResolvedPostHogLogsConfig extends PostHogLogsConfig {
180
+ // Fields PostHogLogs needs resolved at runtime. The host SDK fills in its
181
+ // defaults and hands the resolved config to the PostHogLogs constructor.
182
+ // Flat names internally public API uses `rateCap: { maxLogs, windowMs }`.
183
+ export interface ResolvedPostHogLogsConfig extends Omit<PostHogLogsConfig, 'rateCap'> {
65
184
  maxBufferSize: number
185
+ flushIntervalMs: number
186
+ maxBatchRecordsPerPost: number
187
+ rateCapWindowMs: number
188
+ maxLogsPerInterval?: number
189
+ backgroundFlushBudgetMs: number
190
+ terminationFlushBudgetMs: number
66
191
  }