@zuplo/cli 7.7.8 → 7.7.9

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 (143) hide show
  1. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.d.ts.map +1 -1
  2. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.js +19 -2
  3. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.mjs +19 -2
  4. package/node_modules/@posthog/core/dist/index.d.ts +7 -0
  5. package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
  6. package/node_modules/@posthog/core/dist/index.js +82 -21
  7. package/node_modules/@posthog/core/dist/index.mjs +4 -0
  8. package/node_modules/@posthog/core/dist/logs/index.d.ts +3 -2
  9. package/node_modules/@posthog/core/dist/logs/index.d.ts.map +1 -1
  10. package/node_modules/@posthog/core/dist/logs/index.js +28 -22
  11. package/node_modules/@posthog/core/dist/logs/index.mjs +29 -23
  12. package/node_modules/@posthog/core/dist/logs/logs-utils.js +1 -1
  13. package/node_modules/@posthog/core/dist/logs/logs-utils.mjs +2 -2
  14. package/node_modules/@posthog/core/dist/logs/types.d.ts +2 -2
  15. package/node_modules/@posthog/core/dist/metrics/index.d.ts +5 -2
  16. package/node_modules/@posthog/core/dist/metrics/index.d.ts.map +1 -1
  17. package/node_modules/@posthog/core/dist/metrics/index.js +28 -14
  18. package/node_modules/@posthog/core/dist/metrics/index.mjs +29 -15
  19. package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts.map +1 -1
  20. package/node_modules/@posthog/core/dist/metrics/metrics-utils.js +1 -2
  21. package/node_modules/@posthog/core/dist/metrics/metrics-utils.mjs +2 -3
  22. package/node_modules/@posthog/core/dist/metrics/types.d.ts +7 -0
  23. package/node_modules/@posthog/core/dist/metrics/types.d.ts.map +1 -1
  24. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +32 -4
  25. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
  26. package/node_modules/@posthog/core/dist/posthog-core-stateless.js +68 -20
  27. package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +68 -20
  28. package/node_modules/@posthog/core/dist/traces/config.d.ts +13 -0
  29. package/node_modules/@posthog/core/dist/traces/config.d.ts.map +1 -0
  30. package/node_modules/@posthog/core/dist/traces/config.js +101 -0
  31. package/node_modules/@posthog/core/dist/traces/config.mjs +63 -0
  32. package/node_modules/@posthog/core/dist/traces/context.d.ts +18 -0
  33. package/node_modules/@posthog/core/dist/traces/context.d.ts.map +1 -0
  34. package/node_modules/@posthog/core/dist/traces/context.js +53 -0
  35. package/node_modules/@posthog/core/dist/traces/context.mjs +15 -0
  36. package/node_modules/@posthog/core/dist/traces/ids.d.ts +12 -0
  37. package/node_modules/@posthog/core/dist/traces/ids.d.ts.map +1 -0
  38. package/node_modules/@posthog/core/dist/traces/ids.js +92 -0
  39. package/node_modules/@posthog/core/dist/traces/ids.mjs +42 -0
  40. package/node_modules/@posthog/core/dist/traces/index.d.ts +150 -0
  41. package/node_modules/@posthog/core/dist/traces/index.d.ts.map +1 -0
  42. package/node_modules/@posthog/core/dist/traces/index.js +551 -0
  43. package/node_modules/@posthog/core/dist/traces/index.mjs +513 -0
  44. package/node_modules/@posthog/core/dist/traces/otlp.d.ts +24 -0
  45. package/node_modules/@posthog/core/dist/traces/otlp.d.ts.map +1 -0
  46. package/node_modules/@posthog/core/dist/traces/otlp.js +158 -0
  47. package/node_modules/@posthog/core/dist/traces/otlp.mjs +108 -0
  48. package/node_modules/@posthog/core/dist/traces/sanitize.d.ts +32 -0
  49. package/node_modules/@posthog/core/dist/traces/sanitize.d.ts.map +1 -0
  50. package/node_modules/@posthog/core/dist/traces/sanitize.js +93 -0
  51. package/node_modules/@posthog/core/dist/traces/sanitize.mjs +43 -0
  52. package/node_modules/@posthog/core/dist/traces/span.d.ts +189 -0
  53. package/node_modules/@posthog/core/dist/traces/span.d.ts.map +1 -0
  54. package/node_modules/@posthog/core/dist/traces/span.js +533 -0
  55. package/node_modules/@posthog/core/dist/traces/span.mjs +462 -0
  56. package/node_modules/@posthog/core/dist/traces/traceparent.d.ts +51 -0
  57. package/node_modules/@posthog/core/dist/traces/traceparent.d.ts.map +1 -0
  58. package/node_modules/@posthog/core/dist/traces/traceparent.js +116 -0
  59. package/node_modules/@posthog/core/dist/traces/traceparent.mjs +63 -0
  60. package/node_modules/@posthog/core/dist/traces/types.d.ts +114 -0
  61. package/node_modules/@posthog/core/dist/traces/types.d.ts.map +1 -0
  62. package/node_modules/@posthog/core/dist/traces/types.js +18 -0
  63. package/node_modules/@posthog/core/dist/traces/types.mjs +1 -0
  64. package/node_modules/@posthog/core/dist/utils/backoff.d.ts +23 -0
  65. package/node_modules/@posthog/core/dist/utils/backoff.d.ts.map +1 -0
  66. package/node_modules/@posthog/core/dist/utils/backoff.js +65 -0
  67. package/node_modules/@posthog/core/dist/utils/backoff.mjs +14 -0
  68. package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts +26 -0
  69. package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts.map +1 -0
  70. package/node_modules/@posthog/core/dist/utils/flush-timer.js +67 -0
  71. package/node_modules/@posthog/core/dist/utils/flush-timer.mjs +29 -0
  72. package/node_modules/@posthog/core/dist/utils/json-utils.d.ts +7 -0
  73. package/node_modules/@posthog/core/dist/utils/json-utils.d.ts.map +1 -1
  74. package/node_modules/@posthog/core/dist/utils/json-utils.js +27 -0
  75. package/node_modules/@posthog/core/dist/utils/json-utils.mjs +25 -1
  76. package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts +13 -2
  77. package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts.map +1 -1
  78. package/node_modules/@posthog/core/dist/utils/otlp-resource.js +34 -7
  79. package/node_modules/@posthog/core/dist/utils/otlp-resource.mjs +26 -2
  80. package/node_modules/@posthog/core/dist/utils/retry-after.d.ts +55 -0
  81. package/node_modules/@posthog/core/dist/utils/retry-after.d.ts.map +1 -0
  82. package/node_modules/@posthog/core/dist/utils/retry-after.js +95 -0
  83. package/node_modules/@posthog/core/dist/utils/retry-after.mjs +50 -0
  84. package/node_modules/@posthog/core/package.json +3 -3
  85. package/node_modules/@posthog/core/src/error-tracking/parsers/base.spec.ts +77 -7
  86. package/node_modules/@posthog/core/src/error-tracking/parsers/base.ts +42 -19
  87. package/node_modules/@posthog/core/src/index.ts +18 -0
  88. package/node_modules/@posthog/core/src/logs/index.spec.ts +490 -2
  89. package/node_modules/@posthog/core/src/logs/index.ts +63 -31
  90. package/node_modules/@posthog/core/src/logs/logs-utils.ts +2 -2
  91. package/node_modules/@posthog/core/src/logs/types.ts +2 -2
  92. package/node_modules/@posthog/core/src/metrics/index.spec.ts +304 -0
  93. package/node_modules/@posthog/core/src/metrics/index.ts +53 -16
  94. package/node_modules/@posthog/core/src/metrics/metrics-utils.ts +2 -3
  95. package/node_modules/@posthog/core/src/metrics/types.ts +10 -2
  96. package/node_modules/@posthog/core/src/posthog-core-stateless.ts +157 -38
  97. package/node_modules/@posthog/core/src/traces/config.spec.ts +271 -0
  98. package/node_modules/@posthog/core/src/traces/config.ts +142 -0
  99. package/node_modules/@posthog/core/src/traces/context.ts +30 -0
  100. package/node_modules/@posthog/core/src/traces/ids.spec.ts +115 -0
  101. package/node_modules/@posthog/core/src/traces/ids.ts +75 -0
  102. package/node_modules/@posthog/core/src/traces/index.spec.ts +3187 -0
  103. package/node_modules/@posthog/core/src/traces/index.ts +1060 -0
  104. package/node_modules/@posthog/core/src/traces/live-spans.spec.ts +69 -0
  105. package/node_modules/@posthog/core/src/traces/otlp.spec.ts +368 -0
  106. package/node_modules/@posthog/core/src/traces/otlp.ts +191 -0
  107. package/node_modules/@posthog/core/src/traces/sanitize.ts +116 -0
  108. package/node_modules/@posthog/core/src/traces/span.spec.ts +1122 -0
  109. package/node_modules/@posthog/core/src/traces/span.ts +822 -0
  110. package/node_modules/@posthog/core/src/traces/traceparent.spec.ts +184 -0
  111. package/node_modules/@posthog/core/src/traces/traceparent.ts +172 -0
  112. package/node_modules/@posthog/core/src/traces/types.ts +140 -0
  113. package/node_modules/@posthog/core/src/utils/backoff.ts +44 -0
  114. package/node_modules/@posthog/core/src/utils/flush-timer.ts +50 -0
  115. package/node_modules/@posthog/core/src/utils/json-utils.ts +33 -0
  116. package/node_modules/@posthog/core/src/utils/otlp-resource.spec.ts +55 -9
  117. package/node_modules/@posthog/core/src/utils/otlp-resource.ts +45 -4
  118. package/node_modules/@posthog/core/src/utils/retry-after.ts +135 -0
  119. package/node_modules/@posthog/types/dist/index.d.ts +1 -0
  120. package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
  121. package/node_modules/@posthog/types/dist/posthog-config.d.ts +10 -5
  122. package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
  123. package/node_modules/@posthog/types/dist/traces.d.ts +387 -0
  124. package/node_modules/@posthog/types/dist/traces.d.ts.map +1 -0
  125. package/node_modules/@posthog/types/dist/traces.js +18 -0
  126. package/node_modules/@posthog/types/dist/traces.mjs +1 -0
  127. package/node_modules/@posthog/types/package.json +1 -1
  128. package/node_modules/@posthog/types/src/index.ts +19 -0
  129. package/node_modules/@posthog/types/src/posthog-config.ts +10 -5
  130. package/node_modules/@posthog/types/src/traces.ts +429 -0
  131. package/node_modules/@zuplo/core/package.json +1 -1
  132. package/node_modules/@zuplo/graphql/package.json +1 -1
  133. package/node_modules/@zuplo/openapi-tools/package.json +1 -1
  134. package/node_modules/@zuplo/otel/package.json +1 -1
  135. package/node_modules/@zuplo/runtime/out/esm/{chunk-DRQ5CMVC.js → chunk-KNAYKBEE.js} +80 -80
  136. package/node_modules/@zuplo/runtime/out/esm/chunk-KNAYKBEE.js.map +1 -0
  137. package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
  138. package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
  139. package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
  140. package/node_modules/@zuplo/runtime/package.json +1 -1
  141. package/package.json +11 -11
  142. package/node_modules/@zuplo/runtime/out/esm/chunk-DRQ5CMVC.js.map +0 -1
  143. /package/node_modules/@zuplo/runtime/out/esm/{chunk-DRQ5CMVC.js.LEGAL.txt → chunk-KNAYKBEE.js.LEGAL.txt} +0 -0
@@ -9,7 +9,10 @@ import type {
9
9
  OtlpNumberDataPoint,
10
10
  } from '@posthog/types'
11
11
  import type { Logger } from '../types'
12
- import { isArray, safeSetTimeout } from '../utils'
12
+ import { isArray } from '../utils'
13
+ import { FlushTimer } from '../utils/flush-timer'
14
+ import { RetryAfterWindow } from '../utils/retry-after'
15
+ import { MAX_FLUSH_BACKOFF_MS, NO_JITTER, backoffDelayMs, drawJitter } from '../utils/backoff'
13
16
  import { toOtlpKeyValueList } from '../utils/otlp-any-value'
14
17
  import {
15
18
  DEFAULT_HISTOGRAM_BOUNDS,
@@ -62,7 +65,11 @@ interface SeriesState {
62
65
  */
63
66
  export class PostHogMetrics {
64
67
  private _series = new Map<string, SeriesState>()
65
- private _flushTimer?: ReturnType<typeof safeSetTimeout>
68
+ private readonly _flushTimer = new FlushTimer(() =>
69
+ this.flush().catch((e) => {
70
+ this._logger.error('Metrics flush failed:', e)
71
+ })
72
+ )
66
73
  // Serializes flushes — a manual flush() during an in-flight timer flush
67
74
  // queues behind it instead of racing it for the same window.
68
75
  private _flushPromise: Promise<void> | null = null
@@ -73,6 +80,9 @@ export class PostHogMetrics {
73
80
  // types under one name produces charts that blend both series.
74
81
  private _typeByName = new Map<string, MetricType>()
75
82
  private _typeCollisionWarned = new Set<string>()
83
+ private _retryAfter = new RetryAfterWindow()
84
+ private _consecutiveFlushFailures = 0
85
+ private _flushJitter = NO_JITTER
76
86
  // Bumped by reset(). A flush that was in flight when reset() ran (e.g. it
77
87
  // lost a shutdown race) sees a stale generation when its send settles and
78
88
  // discards its window instead of merging it back and re-arming the timer.
@@ -136,7 +146,10 @@ export class PostHogMetrics {
136
146
  /** Clears the flush timer, drops the current window, and invalidates in-flight flushes. */
137
147
  reset(): void {
138
148
  this._generation++
139
- this._clearFlushTimer()
149
+ this._retryAfter.reset()
150
+ this._consecutiveFlushFailures = 0
151
+ this._flushJitter = NO_JITTER
152
+ this._flushTimer.clear()
140
153
  this._series = new Map()
141
154
  this._flushPromise = null
142
155
  this._seriesCapWarned = false
@@ -284,26 +297,35 @@ export class PostHogMetrics {
284
297
  return result
285
298
  }
286
299
 
300
+ // Every capture calls this, so a pending timer is left alone — re-arming on
301
+ // each one would push the flush out for as long as metrics keep arriving.
287
302
  private _armFlushTimer(): void {
288
- if (this._flushTimer) {
303
+ if (this._flushTimer.pending) {
289
304
  return
290
305
  }
291
- this._flushTimer = safeSetTimeout(() => {
292
- this._flushTimer = undefined
293
- this.flush().catch((e) => {
294
- this._logger.error('Metrics flush failed:', e)
295
- })
296
- }, this._config.flushIntervalMs)
306
+ this._flushTimer.arm(this._nextFlushDelay())
297
307
  }
298
308
 
299
- private _clearFlushTimer(): void {
300
- if (this._flushTimer) {
301
- clearTimeout(this._flushTimer)
302
- this._flushTimer = undefined
303
- }
309
+ // A floor, not a replacement: the header never retries us sooner than our own
310
+ // backoff would have. The backoff doubles per consecutive failure so a
311
+ // sustained outage is not retried every interval, and the jitter is drawn
312
+ // once per failure so a fleet refused together does not return together.
313
+ private _nextFlushDelay(): number {
314
+ return Math.max(
315
+ backoffDelayMs(
316
+ this._config.flushIntervalMs,
317
+ this._consecutiveFlushFailures,
318
+ this._flushJitter,
319
+ MAX_FLUSH_BACKOFF_MS
320
+ ),
321
+ this._retryAfter.remainingMs()
322
+ )
304
323
  }
305
324
 
306
325
  private async _doFlush(): Promise<void> {
326
+ // A flush retires the pending timer, so a delay armed for a window this
327
+ // flush may close cannot outlive it.
328
+ this._flushTimer.clear()
307
329
  if (this._series.size === 0) {
308
330
  return
309
331
  }
@@ -323,6 +345,21 @@ export class PostHogMetrics {
323
345
  // reconfigured, so this window is dropped whatever the outcome was.
324
346
  return
325
347
  }
348
+ this._retryAfter.record(outcome)
349
+ // Before any delay is taken from it, so the timer armed below is measured
350
+ // against this outcome rather than the one before it.
351
+ if (outcome.kind === 'retry-later') {
352
+ this._consecutiveFlushFailures++
353
+ this._flushJitter = drawJitter()
354
+ } else {
355
+ this._consecutiveFlushFailures = 0
356
+ this._flushJitter = NO_JITTER
357
+ }
358
+ // Outright, not through the ratchet: a timer a mid-flight capture armed is
359
+ // measured against a window this outcome may just have closed.
360
+ if (this._flushTimer.pending) {
361
+ this._flushTimer.arm(this._nextFlushDelay())
362
+ }
326
363
  switch (outcome.kind) {
327
364
  case 'ok':
328
365
  return
@@ -331,7 +368,7 @@ export class PostHogMetrics {
331
368
  // the next flush instead of being lost — and re-arm the timer, since
332
369
  // with no new captures nothing else would schedule that flush.
333
370
  this._mergeWindowBack(window)
334
- this._armFlushTimer()
371
+ this._flushTimer.armNoEarlierThan(this._nextFlushDelay())
335
372
  return
336
373
  case 'too-large':
337
374
  this._logger.warn('Metrics batch exceeded the server size limit and was dropped')
@@ -1,6 +1,5 @@
1
1
  import type { MetricAttributeValue, OtlpMetric, OtlpMetricsPayload } from '@posthog/types'
2
- import { toOtlpKeyValueList } from '../utils/otlp-any-value'
3
- import { buildOtlpResourceAttributes } from '../utils/otlp-resource'
2
+ import { buildOtlpResourceAttributes, toOtlpResourceKeyValueList } from '../utils/otlp-resource'
4
3
  import type { ResolvedPostHogMetricsConfig } from './types'
5
4
 
6
5
  /**
@@ -81,7 +80,7 @@ export function buildOtlpMetricsPayload(
81
80
  return {
82
81
  resourceMetrics: [
83
82
  {
84
- resource: { attributes: toOtlpKeyValueList(resourceAttributes) },
83
+ resource: { attributes: toOtlpResourceKeyValueList(resourceAttributes) },
85
84
  scopeMetrics: [
86
85
  {
87
86
  scope: { name: scopeName, version: scopeVersion },
@@ -20,8 +20,16 @@ import type { BeforeSendMetricFn, MetricAttributeValue, OtlpMetricsPayload } fro
20
20
  /** Same tagged outcome shape as `SendLogsBatchOutcome` — one policy for both signals. */
21
21
  export type SendMetricsBatchOutcome =
22
22
  | { kind: 'ok' }
23
- | { kind: 'retry-later'; error: unknown }
24
- | { kind: 'too-large' }
23
+ | { kind: 'retry-later'; error: unknown; retryAfterMs?: number }
24
+ | {
25
+ kind: 'too-large'
26
+ /**
27
+ * True when the SDK measured the body itself rather than the endpoint
28
+ * refusing it, so the caller can split this drain without lowering the
29
+ * batch size it keeps between them.
30
+ */
31
+ measuredLocally?: boolean
32
+ }
25
33
  | { kind: 'fatal'; error: unknown }
26
34
 
27
35
  /**
@@ -1,5 +1,6 @@
1
- import type { OtlpLogsPayload, OtlpMetricsPayload } from '@posthog/types'
1
+ import type { OtlpLogsPayload, OtlpMetricsPayload, OtlpTracesPayload } from '@posthog/types'
2
2
  import type { SendMetricsBatchOutcome } from './metrics/types'
3
+ import type { SendTracesBatchOutcome } from './traces/types'
3
4
  import { SimpleEventEmitter } from './eventemitter'
4
5
  import { getFeatureFlagValue, minimizeFlagCalledEventProperties, normalizeFlagsResponse } from './featureFlagUtils'
5
6
  import { gzipCompress, isGzipSupported } from './gzip'
@@ -40,6 +41,7 @@ import {
40
41
  getEventUuid,
41
42
  safeJsonStringify,
42
43
  } from './utils'
44
+ import { parseRetryAfterMs } from './utils/retry-after'
43
45
  import { uuidv7 } from './vendor/uuidv7'
44
46
  import {
45
47
  ErrorPropertiesBuilder,
@@ -69,6 +71,20 @@ class PostHogFetchHttpError extends Error {
69
71
  return this.response.status
70
72
  }
71
73
 
74
+ /**
75
+ * The response's `Retry-After` as milliseconds from now, when it sent a usable
76
+ * one, clamped to `MAX_RETRY_AFTER_MS`.
77
+ */
78
+ get retryAfterMs(): number | undefined {
79
+ try {
80
+ return parseRetryAfterMs(this.response.headers?.get('retry-after'))
81
+ } catch {
82
+ // `headers.get` is injected transport code; a throwing one must not turn a
83
+ // retriable failure into an unhandled rejection.
84
+ return undefined
85
+ }
86
+ }
87
+
72
88
  get bodyReadTimedOut(): boolean {
73
89
  return this._bodyReadTimedOut
74
90
  }
@@ -205,6 +221,46 @@ function isRetryableFlagsFetchError(
205
221
  return code !== 'ECONNREFUSED'
206
222
  }
207
223
 
224
+ /**
225
+ * Ceiling on what the SDK will put on the wire: a body over it is reported as
226
+ * too large without a request being made, and a batch of one that still exceeds
227
+ * it is dropped. The ingestion service decompresses a `Content-Encoding: gzip`
228
+ * request before it applies its own `MAX_REQUEST_BODY_SIZE_BYTES`, so the size
229
+ * that has to stay under the limit is the uncompressed one measured here.
230
+ *
231
+ * Set to the largest limit any known deployment configures — 10 MiB, what the
232
+ * ingestion service runs with — rather than the 2 MB the service falls back to
233
+ * when nothing configures it. The ceiling only earns its place by refusing a
234
+ * body that no deployment would have accepted: at 2 MB it would instead refuse
235
+ * bodies the service takes today, dropping records with no `413` to show for
236
+ * them. Deployments configured lower, and proxies in front of them, are covered
237
+ * by the `413` path, which stays the primary mechanism.
238
+ */
239
+ const OTLP_MAX_BODY_BYTES = 10 * 1024 * 1024
240
+
241
+ /**
242
+ * A request body's size on the wire. `Buffer` where it exists, `TextEncoder`
243
+ * elsewhere.
244
+ *
245
+ * Total by construction: it runs on hosts that define only part of the web
246
+ * platform — `Blob` in particular is absent on some server runtimes — and a
247
+ * size that cannot be measured is reported as `0`, leaving the body to be sent
248
+ * rather than turning a missing global into a failed export.
249
+ */
250
+ function byteLengthOf(body: string | Blob | Uint8Array): number {
251
+ try {
252
+ if (typeof body !== 'string') {
253
+ return body instanceof Uint8Array ? body.byteLength : body.size
254
+ }
255
+ if (typeof Buffer !== 'undefined') {
256
+ return Buffer.byteLength(body, STRING_FORMAT)
257
+ }
258
+ return new TextEncoder().encode(body).length
259
+ } catch {
260
+ return 0
261
+ }
262
+ }
263
+
208
264
  export function isPostHogFetchContentTooLargeError(err: unknown): err is PostHogFetchHttpError & { status: 413 } {
209
265
  return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413
210
266
  }
@@ -232,19 +288,35 @@ function isPostHogEventProperties(value: JsonType | undefined): value is PostHog
232
288
  */
233
289
  export type SendLogsBatchOutcome =
234
290
  | { kind: 'ok' }
235
- | { kind: 'too-large' }
236
- | { kind: 'retry-later'; error: unknown }
291
+ | {
292
+ kind: 'too-large'
293
+ /**
294
+ * True when the SDK measured the body itself rather than the endpoint
295
+ * refusing it, so the caller can split this drain without lowering the
296
+ * batch size it keeps between them.
297
+ */
298
+ measuredLocally?: boolean
299
+ }
300
+ | { kind: 'retry-later'; error: unknown; retryAfterMs?: number }
237
301
  | { kind: 'fatal'; error: unknown }
238
302
 
239
303
  /**
240
304
  * Each signal keeps its own exported outcome type because each belongs to a
241
- * separate host contract. The wrappers return this value directly, so one
242
- * drifting out of shape fails to compile.
305
+ * separate host contract. The wrappers return this value directly, so any of
306
+ * the three drifting out of shape fails to compile.
243
307
  */
244
308
  type SendOtlpBatchOutcome =
245
309
  | { kind: 'ok' }
246
- | { kind: 'too-large' }
247
- | { kind: 'retry-later'; error: unknown }
310
+ | {
311
+ kind: 'too-large'
312
+ /**
313
+ * True when the SDK measured the body itself rather than the endpoint
314
+ * refusing it, so the caller can split this drain without lowering the
315
+ * batch size it keeps between them.
316
+ */
317
+ measuredLocally?: boolean
318
+ }
319
+ | { kind: 'retry-later'; error: unknown; retryAfterMs?: number }
248
320
  | { kind: 'fatal'; error: unknown }
249
321
 
250
322
  export enum QuotaLimitedFeature {
@@ -1326,11 +1398,20 @@ export abstract class PostHogCoreStateless {
1326
1398
  if (this.pendingFlushPromise) {
1327
1399
  return
1328
1400
  }
1329
- void this.flush().catch(async (err) => {
1401
+ void this.flushAutomatic().catch(async (err) => {
1330
1402
  await logFlushError(err)
1331
1403
  })
1332
1404
  }
1333
1405
 
1406
+ /**
1407
+ * The flush the SDK runs on its own, from the interval timer or the `flushAt`
1408
+ * threshold. Separate from `flush()` so a host can hold back work that an
1409
+ * endpoint has asked it to wait on, which an explicit flush overrides.
1410
+ */
1411
+ protected flushAutomatic(): Promise<void> {
1412
+ return this.flush()
1413
+ }
1414
+
1334
1415
  private async waitForPendingPromises(
1335
1416
  maxPromiseId: number,
1336
1417
  ignoredPromises: (Promise<any> | null | undefined)[] = []
@@ -1642,9 +1723,9 @@ export abstract class PostHogCoreStateless {
1642
1723
  }
1643
1724
 
1644
1725
  /**
1645
- * Shared implementation behind the OTLP senders, which differ only in path.
1646
- * Returns a tagged outcome instead of throwing so the queue owners don't
1647
- * have to know the core's error class hierarchy.
1726
+ * Shared implementation behind the three OTLP senders, which differ only in
1727
+ * path and auth style. Returns a tagged outcome instead of throwing so the
1728
+ * queue owners don't have to know the core's error class hierarchy.
1648
1729
  *
1649
1730
  * Exhausted 408/429/5xx stay `retry-later`, unlike the events `_flush()`
1650
1731
  * which drops anything that isn't a network error: every OTLP queue is
@@ -1653,27 +1734,64 @@ export abstract class PostHogCoreStateless {
1653
1734
  */
1654
1735
  private async _sendOtlpBatch({
1655
1736
  path,
1737
+ auth,
1656
1738
  payload,
1657
1739
  }: {
1658
- path: 'logs' | 'metrics'
1659
- payload: OtlpLogsPayload | OtlpMetricsPayload
1740
+ path: 'logs' | 'metrics' | 'traces'
1741
+ auth: 'query-token' | 'bearer'
1742
+ payload: OtlpLogsPayload | OtlpMetricsPayload | OtlpTracesPayload
1660
1743
  }): Promise<SendOtlpBatchOutcome> {
1661
1744
  if (this.disabled) {
1662
1745
  return { kind: 'fatal', error: new Error('The client is disabled') }
1663
1746
  }
1664
1747
 
1665
- const serialized = JSON.stringify(payload)
1666
- const url = `${this.host}/i/v1/${path}?token=${encodeURIComponent(this.apiKey)}`
1748
+ // Serialised behind a guard: a payload too big to hold as one string throws
1749
+ // `RangeError` here, which escapes the tagged-outcome contract and leaves the
1750
+ // caller retrying a batch it can never send. Reported as too-large so it takes
1751
+ // the same halve-and-isolate path as a batch that serialises but is oversized.
1752
+ let serialized: string
1753
+ try {
1754
+ serialized = JSON.stringify(payload)
1755
+ } catch (error) {
1756
+ this.logMsgIfDebug(() =>
1757
+ console.warn(`[PostHog] Could not serialize a ${path} batch; reporting it as too large`, error)
1758
+ )
1759
+ return { kind: 'too-large', measuredLocally: true }
1760
+ }
1761
+
1762
+ // Measured on the uncompressed payload: the endpoint decompresses the body
1763
+ // and applies its limit to what comes out, so one that gzips small is still
1764
+ // refused on its decompressed size. A batch the endpoint cannot accept is
1765
+ // reported without being sent — and before it is compressed — so the caller
1766
+ // halves it, and ultimately isolates and drops the one oversized record,
1767
+ // without spending a request or a gzip pass on each attempt.
1768
+ const payloadBytes = byteLengthOf(serialized)
1769
+ if (payloadBytes > OTLP_MAX_BODY_BYTES) {
1770
+ this.logMsgIfDebug(() =>
1771
+ console.warn(
1772
+ `[PostHog] Not sending a ${path} batch of ${payloadBytes} bytes: the endpoint accepts at most ${OTLP_MAX_BODY_BYTES}`
1773
+ )
1774
+ )
1775
+ return { kind: 'too-large', measuredLocally: true }
1776
+ }
1777
+
1778
+ const url =
1779
+ auth === 'bearer'
1780
+ ? `${this.host}/i/v1/${path}`
1781
+ : `${this.host}/i/v1/${path}?token=${encodeURIComponent(this.apiKey)}`
1667
1782
 
1668
1783
  const gzippedPayload = !this.disableCompression ? await this.compressPayload(serialized) : null
1784
+ const body = gzippedPayload || serialized
1785
+
1669
1786
  const fetchOptions: PostHogFetchOptions = {
1670
1787
  method: 'POST',
1671
1788
  headers: {
1672
1789
  ...this.getCustomHeaders(),
1673
1790
  'Content-Type': 'application/json',
1791
+ ...(auth === 'bearer' && { Authorization: `Bearer ${this.apiKey}` }),
1674
1792
  ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
1675
1793
  },
1676
- body: gzippedPayload || serialized,
1794
+ body,
1677
1795
  }
1678
1796
 
1679
1797
  try {
@@ -1686,6 +1804,12 @@ export abstract class PostHogCoreStateless {
1686
1804
  if (isPostHogFetchContentTooLargeError(err)) {
1687
1805
  return false
1688
1806
  }
1807
+ if (err instanceof PostHogFetchHttpError && err.retryAfterMs !== undefined) {
1808
+ // The endpoint named a wait. This loop retries on a fixed short
1809
+ // delay, so retrying here would spend every attempt inside the
1810
+ // window; hand it to the queue's backoff instead.
1811
+ return false
1812
+ }
1689
1813
  return isPostHogFetchRetryableError(err)
1690
1814
  },
1691
1815
  }
@@ -1696,18 +1820,31 @@ export abstract class PostHogCoreStateless {
1696
1820
  return { kind: 'too-large' }
1697
1821
  }
1698
1822
  if (isPostHogFetchRetryableError(err)) {
1699
- return { kind: 'retry-later', error: err }
1823
+ const retryAfterMs = err instanceof PostHogFetchHttpError ? err.retryAfterMs : undefined
1824
+ return { kind: 'retry-later', error: err, ...(retryAfterMs !== undefined && { retryAfterMs }) }
1700
1825
  }
1701
1826
  return { kind: 'fatal', error: err }
1702
1827
  }
1703
1828
  }
1704
1829
 
1705
1830
  async _sendLogsBatch(payload: OtlpLogsPayload): Promise<SendLogsBatchOutcome> {
1706
- return this._sendOtlpBatch({ path: 'logs', payload })
1831
+ return this._sendOtlpBatch({ path: 'logs', auth: 'query-token', payload })
1707
1832
  }
1708
1833
 
1709
1834
  async _sendMetricsBatch(payload: OtlpMetricsPayload): Promise<SendMetricsBatchOutcome> {
1710
- return this._sendOtlpBatch({ path: 'metrics', payload })
1835
+ return this._sendOtlpBatch({ path: 'metrics', auth: 'query-token', payload })
1836
+ }
1837
+
1838
+ /**
1839
+ * The `TracesHost._sendTracesBatch` implementation, so `PostHogTraces` can
1840
+ * use any core-based SDK as its host.
1841
+ *
1842
+ * Authenticates with `Authorization: Bearer` rather than the `?token=` query
1843
+ * parameter the logs and metrics senders use: it's the service's primary auth
1844
+ * path, and server runtimes have no CORS preflight to avoid.
1845
+ */
1846
+ async _sendTracesBatch(payload: OtlpTracesPayload): Promise<SendTracesBatchOutcome> {
1847
+ return this._sendOtlpBatch({ path: 'traces', auth: 'bearer', payload })
1711
1848
  }
1712
1849
 
1713
1850
  private fetchWithRetry<T>(
@@ -1732,25 +1869,7 @@ export abstract class PostHogCoreStateless {
1732
1869
  requestTimeout?: number
1733
1870
  ): Promise<T | void> {
1734
1871
  const body = options.body ? options.body : ''
1735
- let reqByteLength = -1
1736
- try {
1737
- if (body instanceof Blob) {
1738
- reqByteLength = body.size
1739
- } else if (body instanceof Uint8Array) {
1740
- reqByteLength = body.byteLength
1741
- } else {
1742
- reqByteLength = Buffer.byteLength(body, STRING_FORMAT)
1743
- }
1744
- } catch {
1745
- if (body instanceof Blob) {
1746
- reqByteLength = body.size
1747
- } else if (body instanceof Uint8Array) {
1748
- reqByteLength = body.byteLength
1749
- } else {
1750
- const encoded = new TextEncoder().encode(body)
1751
- reqByteLength = encoded.length
1752
- }
1753
- }
1872
+ const reqByteLength = byteLengthOf(body)
1754
1873
 
1755
1874
  const retriableOptions = { ...this._retryOptions, ...retryOptions }
1756
1875
  let attempt = 0