@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
@@ -164,6 +164,9 @@ describe('PostHogLogs', () => {
164
164
  beforeEach(() => {
165
165
  mockInstance = createMockInstance()
166
166
  logger = createMockLogger()
167
+ // Retry delays carry jitter; pinned to its midpoint so every timing
168
+ // assertion here measures the backoff itself and cannot flake.
169
+ vi.spyOn(Math, 'random').mockReturnValue(0.5)
167
170
  })
168
171
 
169
172
  it('constructs without throwing', () => {
@@ -927,7 +930,7 @@ describe('PostHogLogs', () => {
927
930
  await logs.flush()
928
931
 
929
932
  expect(logger.warn).toHaveBeenCalledWith(
930
- expect.stringContaining('Dropping a single log record after 413 with batch size 1')
933
+ expect.stringContaining('Dropping a single log record with batch size 1')
931
934
  )
932
935
  })
933
936
 
@@ -985,7 +988,7 @@ describe('PostHogLogs', () => {
985
988
  expect(sendSizes).toEqual([1])
986
989
  expect(readQueue(mockInstance)).toHaveLength(0)
987
990
  expect(logger.warn).toHaveBeenCalledWith(
988
- expect.stringContaining('Dropping a single log record after 413 with batch size 1')
991
+ expect.stringContaining('Dropping a single log record with batch size 1')
989
992
  )
990
993
  })
991
994
 
@@ -1166,6 +1169,468 @@ describe('PostHogLogs', () => {
1166
1169
  expect(readQueue(mockInstance)).toHaveLength(0)
1167
1170
  })
1168
1171
 
1172
+ it('waits out Retry-After even when a capture re-arms the timer mid-flush', async () => {
1173
+ // The capture that lands while the send is in flight arms a timer at the
1174
+ // plain interval; the 429 then asks for far longer. The earlier timer must
1175
+ // not fire first, or the SDK sends inside the window it was told to skip.
1176
+ mockInstance._sendLogsBatch = vi.fn(async () => {
1177
+ logs.captureLog({ body: 'arrived mid-flush' })
1178
+ return { kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }
1179
+ })
1180
+
1181
+ const logs = new PostHogLogs(
1182
+ mockInstance,
1183
+ resolveForTest({ flushIntervalMs: 5000 }),
1184
+ logger,
1185
+ getContextFor(mockInstance),
1186
+ immediateOnReady
1187
+ )
1188
+ logs.captureLog({ body: 'first' })
1189
+
1190
+ await vi.advanceTimersByTimeAsync(5000)
1191
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1192
+
1193
+ await vi.advanceTimersByTimeAsync(6000)
1194
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1195
+
1196
+ await vi.advanceTimersByTimeAsync(300_000)
1197
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1198
+ })
1199
+
1200
+ it('does not let the size trigger send inside a Retry-After window', async () => {
1201
+ mockInstance._sendLogsBatch = vi.fn(() =>
1202
+ Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1203
+ )
1204
+ const logs = new PostHogLogs(
1205
+ mockInstance,
1206
+ resolveForTest({ flushIntervalMs: 1000, maxBufferSize: 2 }),
1207
+ logger,
1208
+ getContextFor(mockInstance),
1209
+ immediateOnReady
1210
+ )
1211
+ logs.captureLog({ body: 'first' })
1212
+ await vi.advanceTimersByTimeAsync(1000)
1213
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1214
+
1215
+ // Enough records to trip the size trigger, well inside the window.
1216
+ logs.captureLog({ body: 'second' })
1217
+ logs.captureLog({ body: 'third' })
1218
+ await vi.advanceTimersByTimeAsync(1000)
1219
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1220
+
1221
+ await vi.advanceTimersByTimeAsync(300_000)
1222
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1223
+ })
1224
+
1225
+ it('does not let onReconnect send inside a Retry-After window', async () => {
1226
+ // `online` fires on every network handover; it says nothing about the
1227
+ // rate-limit window the endpoint set.
1228
+ mockInstance._sendLogsBatch = vi.fn(() =>
1229
+ Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1230
+ )
1231
+ const logs = new PostHogLogs(
1232
+ mockInstance,
1233
+ resolveForTest({ flushIntervalMs: 1000 }),
1234
+ logger,
1235
+ getContextFor(mockInstance),
1236
+ immediateOnReady
1237
+ )
1238
+ logs.captureLog({ body: 'first' })
1239
+ await vi.advanceTimersByTimeAsync(1000)
1240
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1241
+
1242
+ logs.onReconnect()
1243
+ await vi.advanceTimersByTimeAsync(1000)
1244
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1245
+ })
1246
+
1247
+ it('flushes on reconnect once the Retry-After window has passed', async () => {
1248
+ const outcomes: any[] = [{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 5000 }]
1249
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' }))
1250
+ const logs = new PostHogLogs(
1251
+ mockInstance,
1252
+ resolveForTest({ flushIntervalMs: 1000 }),
1253
+ logger,
1254
+ getContextFor(mockInstance),
1255
+ immediateOnReady
1256
+ )
1257
+ logs.captureLog({ body: 'first' })
1258
+ await vi.advanceTimersByTimeAsync(1000)
1259
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1260
+
1261
+ // Stop just short of the deadline, then cross it without letting the
1262
+ // re-armed timer fire — otherwise the timer satisfies the assertion and
1263
+ // the test says nothing about onReconnect.
1264
+ await vi.advanceTimersByTimeAsync(4999)
1265
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1266
+ vi.setSystemTime(Date.now() + 2)
1267
+
1268
+ logs.onReconnect()
1269
+ await vi.advanceTimersByTimeAsync(1)
1270
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1271
+ })
1272
+
1273
+ it('does not let a capture after an explicit flush send inside the window', async () => {
1274
+ // `flush()` is the lifecycle path (RN foreground/background, shutdown).
1275
+ // It leaves no timer behind, so the next capture is the one that arms
1276
+ // one — at the plain interval unless the window floors it.
1277
+ mockInstance._sendLogsBatch = vi.fn(() =>
1278
+ Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1279
+ )
1280
+ const logs = new PostHogLogs(
1281
+ mockInstance,
1282
+ resolveForTest({ flushIntervalMs: 5000 }),
1283
+ logger,
1284
+ getContextFor(mockInstance),
1285
+ immediateOnReady
1286
+ )
1287
+ logs.captureLog({ body: 'first' })
1288
+ await logs.flush().catch(() => {})
1289
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1290
+
1291
+ logs.captureLog({ body: 'second' })
1292
+ await vi.advanceTimersByTimeAsync(5000)
1293
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1294
+
1295
+ await vi.advanceTimersByTimeAsync(295_000)
1296
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1297
+ })
1298
+
1299
+ it('does not let a capture during an explicit flush send inside the window', async () => {
1300
+ // The sibling case covers a capture *after* the flush settles. This one
1301
+ // lands while the send is in flight, so it arms the timer at the plain
1302
+ // interval before the window exists.
1303
+ let release: (v: any) => void = () => {}
1304
+ mockInstance._sendLogsBatch = vi.fn(
1305
+ () =>
1306
+ new Promise((resolve) => {
1307
+ release = resolve
1308
+ })
1309
+ )
1310
+ const logs = new PostHogLogs(
1311
+ mockInstance,
1312
+ resolveForTest({ flushIntervalMs: 5000 }),
1313
+ logger,
1314
+ getContextFor(mockInstance),
1315
+ immediateOnReady
1316
+ )
1317
+ logs.captureLog({ body: 'first' })
1318
+ const flushed = logs.flush().catch(() => {})
1319
+ await vi.advanceTimersByTimeAsync(0)
1320
+
1321
+ logs.captureLog({ body: 'second' })
1322
+ release({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1323
+ await flushed
1324
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1325
+
1326
+ await vi.advanceTimersByTimeAsync(5000)
1327
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1328
+ })
1329
+
1330
+ it('arms a timer when onReconnect lands inside the window after an explicit flush', async () => {
1331
+ // `flush()` leaves no timer behind, so returning early here without
1332
+ // arming one leaves the records with nothing scheduled at all.
1333
+ mockInstance._sendLogsBatch = vi.fn(() =>
1334
+ Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1335
+ )
1336
+ const logs = new PostHogLogs(
1337
+ mockInstance,
1338
+ resolveForTest({ flushIntervalMs: 10_000 }),
1339
+ logger,
1340
+ getContextFor(mockInstance),
1341
+ immediateOnReady
1342
+ )
1343
+ logs.captureLog({ body: 'first' })
1344
+ await logs.flush().catch(() => {})
1345
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1346
+
1347
+ await vi.advanceTimersByTimeAsync(10_000)
1348
+ logs.onReconnect()
1349
+
1350
+ await vi.advanceTimersByTimeAsync(289_000)
1351
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1352
+ await vi.advanceTimersByTimeAsync(2000)
1353
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1354
+ })
1355
+
1356
+ it('releases a record captured mid-flush once that flush closes the window', async () => {
1357
+ // The capture arms against the window that was open when it landed; the
1358
+ // outcome then closes that window, so the timer has to come back down.
1359
+ const logs = new PostHogLogs(
1360
+ mockInstance,
1361
+ resolveForTest({ flushIntervalMs: 10_000 }),
1362
+ logger,
1363
+ getContextFor(mockInstance),
1364
+ immediateOnReady
1365
+ )
1366
+ let sends = 0
1367
+ mockInstance._sendLogsBatch = vi.fn(async () => {
1368
+ sends += 1
1369
+ if (sends === 1) {
1370
+ return { kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }
1371
+ }
1372
+ await Promise.resolve()
1373
+ if (sends === 2) {
1374
+ logs.captureLog({ body: 'mid' })
1375
+ }
1376
+ return { kind: 'ok' }
1377
+ })
1378
+
1379
+ logs.captureLog({ body: 'first' })
1380
+ await vi.advanceTimersByTimeAsync(10_000)
1381
+ await logs.flush().catch(() => {})
1382
+
1383
+ await vi.advanceTimersByTimeAsync(10_000)
1384
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(3)
1385
+ })
1386
+
1387
+ it('keeps the Retry-After window when a batch is refused for size', async () => {
1388
+ // `too-large` is a verdict on the body's size — the SDK's own or a 413 —
1389
+ // so it says nothing about the endpoint's rate limit and must not end the
1390
+ // wait.
1391
+ const outcomes: any[] = [{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }]
1392
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'too-large' }))
1393
+ const logs = new PostHogLogs(
1394
+ mockInstance,
1395
+ resolveForTest({ flushIntervalMs: 1000, maxBatchRecordsPerPost: 1 }),
1396
+ logger,
1397
+ getContextFor(mockInstance),
1398
+ immediateOnReady
1399
+ )
1400
+ logs.captureLog({ body: 'first' })
1401
+ await vi.advanceTimersByTimeAsync(1000)
1402
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1403
+
1404
+ // A batch of one the endpoint cannot accept: the record is dropped.
1405
+ await logs.flush()
1406
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1407
+
1408
+ logs.captureLog({ body: 'second' })
1409
+ logs.onReconnect()
1410
+ await vi.advanceTimersByTimeAsync(1000)
1411
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1412
+ })
1413
+
1414
+ it('ends the window when an explicit flush succeeds', async () => {
1415
+ // The endpoint just accepted a batch, so the wait it asked for earlier is
1416
+ // over — the gated paths must not stay blocked for the rest of it.
1417
+ const outcomes: any[] = [{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }]
1418
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' }))
1419
+ const logs = new PostHogLogs(
1420
+ mockInstance,
1421
+ resolveForTest({ flushIntervalMs: 5000, maxBufferSize: 2 }),
1422
+ logger,
1423
+ getContextFor(mockInstance),
1424
+ immediateOnReady
1425
+ )
1426
+ logs.captureLog({ body: 'first' })
1427
+ await vi.advanceTimersByTimeAsync(5000)
1428
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1429
+
1430
+ await logs.flush()
1431
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1432
+
1433
+ logs.captureLog({ body: 'second' })
1434
+ logs.onReconnect()
1435
+ await vi.advanceTimersByTimeAsync(1)
1436
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(3)
1437
+ })
1438
+
1439
+ it('drops a Retry-After wait on reset', async () => {
1440
+ const outcomes: any[] = [{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }]
1441
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' }))
1442
+ const logs = new PostHogLogs(
1443
+ mockInstance,
1444
+ resolveForTest({ flushIntervalMs: 1000 }),
1445
+ logger,
1446
+ getContextFor(mockInstance),
1447
+ immediateOnReady
1448
+ )
1449
+ logs.captureLog({ body: 'first' })
1450
+ await vi.advanceTimersByTimeAsync(1000)
1451
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1452
+
1453
+ // Asserted through a gated path: a plain capture would flush either way.
1454
+ logs.reset()
1455
+ logs.captureLog({ body: 'second' })
1456
+ logs.onReconnect()
1457
+ await vi.advanceTimersByTimeAsync(1)
1458
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1459
+ })
1460
+
1461
+ it('keeps its own backoff when the endpoint asks for less', async () => {
1462
+ // A proxy answering `Retry-After: 1` must not turn the retry into a
1463
+ // one-second hot loop against an endpoint already refusing traffic.
1464
+ mockInstance._sendLogsBatch = vi.fn(() =>
1465
+ Promise.resolve({ kind: 'retry-later', error: new Error('503'), retryAfterMs: 10 })
1466
+ )
1467
+ const logs = new PostHogLogs(
1468
+ mockInstance,
1469
+ resolveForTest({ flushIntervalMs: 5000 }),
1470
+ logger,
1471
+ getContextFor(mockInstance),
1472
+ immediateOnReady
1473
+ )
1474
+ logs.captureLog({ body: 'first' })
1475
+ await vi.advanceTimersByTimeAsync(5000)
1476
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1477
+
1478
+ await vi.advanceTimersByTimeAsync(1000)
1479
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1480
+
1481
+ await vi.advanceTimersByTimeAsync(5000)
1482
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1483
+ })
1484
+
1485
+ it('does not restart a served-out wait for a capture during the retry', async () => {
1486
+ // A deadline, not a duration. The retry's timer has already fired, so the
1487
+ // capture below arms the next one — and it sees the wait still set,
1488
+ // because the send it belongs to has not settled. Holding a duration here
1489
+ // re-arms for the whole window again and leaves the record 300s behind an
1490
+ // endpoint that has already recovered.
1491
+ let settle: ((outcome: any) => void) | undefined
1492
+ let call = 0
1493
+ mockInstance._sendLogsBatch = vi.fn(() => {
1494
+ call++
1495
+ if (call === 1) {
1496
+ return Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
1497
+ }
1498
+ return new Promise((resolve) => {
1499
+ settle = resolve
1500
+ })
1501
+ })
1502
+ const logs = new PostHogLogs(
1503
+ mockInstance,
1504
+ resolveForTest({ flushIntervalMs: 5000 }),
1505
+ logger,
1506
+ getContextFor(mockInstance),
1507
+ immediateOnReady
1508
+ )
1509
+ logs.captureLog({ body: 'first' })
1510
+ await vi.advanceTimersByTimeAsync(5000)
1511
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1512
+
1513
+ // The wait elapses and the retry goes out, but hangs.
1514
+ await vi.advanceTimersByTimeAsync(300_000)
1515
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1516
+
1517
+ logs.captureLog({ body: 'second' })
1518
+ settle?.({ kind: 'ok' })
1519
+ await vi.advanceTimersByTimeAsync(0)
1520
+
1521
+ // One interval, not another window.
1522
+ await vi.advanceTimersByTimeAsync(5000)
1523
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(3)
1524
+ })
1525
+
1526
+ it('closes the window at the ceiling for a host out-pacing it', async () => {
1527
+ // RN takes flush() on every app-state transition, and each refusal pushes
1528
+ // the deadline out. The ceiling is what stops the gated paths — the size
1529
+ // trigger and onReconnect — from being suppressed for good.
1530
+ mockInstance._sendLogsBatch = vi.fn(() =>
1531
+ Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 30_000 })
1532
+ )
1533
+ const logs = new PostHogLogs(
1534
+ mockInstance,
1535
+ resolveForTest({ flushIntervalMs: 60_000 }),
1536
+ logger,
1537
+ getContextFor(mockInstance),
1538
+ immediateOnReady
1539
+ )
1540
+ logs.captureLog({ body: 'first' })
1541
+ await logs.flush().catch(() => {})
1542
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1543
+
1544
+ // Lifecycle flushes every 5s against a 30s window, for longer than the
1545
+ // 5-minute ceiling. Sampled rather than asserted through onReconnect:
1546
+ // whether a given moment falls inside a window is timing-dependent, but
1547
+ // past the ceiling it must fall outside one.
1548
+ let sawWindowClosed = false
1549
+ for (let i = 0; i < 70; i++) {
1550
+ await vi.advanceTimersByTimeAsync(5000)
1551
+ // Sampled before the flush: a flush that finds the window closed opens
1552
+ // a fresh one, so sampling after it would always look open.
1553
+ if ((logs as any)._retryAfter.remainingMs() === 0) {
1554
+ sawWindowClosed = true
1555
+ }
1556
+ logs.captureLog({ body: `line ${i}` })
1557
+ await logs.flush().catch(() => {})
1558
+ }
1559
+ expect(sawWindowClosed).toBe(true)
1560
+ })
1561
+
1562
+ it('keeps flushing after a backward clock step', async () => {
1563
+ const outcomes: any[] = [{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 60_000 }]
1564
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' }))
1565
+ const logs = new PostHogLogs(
1566
+ mockInstance,
1567
+ resolveForTest({ flushIntervalMs: 1000, maxBufferSize: 2 }),
1568
+ logger,
1569
+ getContextFor(mockInstance),
1570
+ immediateOnReady
1571
+ )
1572
+ logs.captureLog({ body: 'first' })
1573
+ await vi.advanceTimersByTimeAsync(1000)
1574
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1575
+
1576
+ const real = Date.now()
1577
+ vi.spyOn(Date, 'now').mockImplementation(() => real - 3_600_000)
1578
+
1579
+ // A gated path: suppressed for the size of the step without the guard.
1580
+ logs.captureLog({ body: 'second' })
1581
+ logs.onReconnect()
1582
+ await vi.advanceTimersByTimeAsync(1)
1583
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1584
+ })
1585
+
1586
+ it('ends the wait when a later failure names none', async () => {
1587
+ // 429 with a window, then a plain 503: the queue drops back to its own
1588
+ // backoff rather than waiting the old window out on every attempt.
1589
+ const outcomes: any[] = [
1590
+ { kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
1591
+ { kind: 'retry-later', error: new Error('503') },
1592
+ ]
1593
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' }))
1594
+ const logs = new PostHogLogs(
1595
+ mockInstance,
1596
+ resolveForTest({ flushIntervalMs: 1000 }),
1597
+ logger,
1598
+ getContextFor(mockInstance),
1599
+ immediateOnReady
1600
+ )
1601
+ logs.captureLog({ body: 'first' })
1602
+ await vi.advanceTimersByTimeAsync(1000)
1603
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1604
+
1605
+ await vi.advanceTimersByTimeAsync(300_000)
1606
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(2)
1607
+
1608
+ // Back on the plain backoff, not another 300s.
1609
+ await vi.advanceTimersByTimeAsync(4000)
1610
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(3)
1611
+ })
1612
+
1613
+ it('keeps flushing on the interval while captures keep arriving', async () => {
1614
+ // Every capture arms the timer. Re-arming a pending one would push the
1615
+ // flush out for as long as logs keep coming, stranding a steady stream
1616
+ // that never reaches the size trigger.
1617
+ const logs = new PostHogLogs(
1618
+ mockInstance,
1619
+ resolveForTest({ flushIntervalMs: 10_000, maxBufferSize: 100 }),
1620
+ logger,
1621
+ getContextFor(mockInstance),
1622
+ immediateOnReady
1623
+ )
1624
+
1625
+ for (let i = 0; i < 30; i++) {
1626
+ logs.captureLog({ body: `line ${i}` })
1627
+ await vi.advanceTimersByTimeAsync(2000)
1628
+ }
1629
+
1630
+ expect(mockInstance._sendLogsBatch).toHaveBeenCalled()
1631
+ expect(readQueue(mockInstance)).toHaveLength(0)
1632
+ })
1633
+
1169
1634
  it('stops re-arming once the queue is empty', async () => {
1170
1635
  const logs = new PostHogLogs(
1171
1636
  mockInstance,
@@ -1185,6 +1650,29 @@ describe('PostHogLogs', () => {
1185
1650
  expect(mockInstance._sendLogsBatch).toHaveBeenCalledTimes(1)
1186
1651
  })
1187
1652
 
1653
+ it('caps the retry delay at 30s however long the outage runs', async () => {
1654
+ // The logs contract states the backoff as capped at ~30s. Without the cap
1655
+ // a 5s interval reaches 320s after six doublings.
1656
+ mockInstance._sendLogsBatch = vi.fn(() => Promise.resolve({ kind: 'retry-later', error: new Error('down') }))
1657
+ const logs = new PostHogLogs(
1658
+ mockInstance,
1659
+ resolveForTest({ flushIntervalMs: 5000 }),
1660
+ logger,
1661
+ getContextFor(mockInstance),
1662
+ immediateOnReady
1663
+ )
1664
+ logs.captureLog({ body: 'retry-me' })
1665
+
1666
+ // Ten failures, well past the six doublings the exponent allows.
1667
+ for (let i = 0; i < 10; i++) {
1668
+ await vi.advanceTimersByTimeAsync(30_000)
1669
+ }
1670
+ // Capped, the delay settles at 30s and 300s of outage buys ten retries on
1671
+ // top of the two the first doublings allow. Uncapped it reaches 320s and
1672
+ // buys six attempts in total, so the difference is not a rounding one.
1673
+ expect(mockInstance._sendLogsBatch.mock.calls.length).toBeGreaterThanOrEqual(11)
1674
+ })
1675
+
1188
1676
  it('backs off exponentially across consecutive failed flushes', async () => {
1189
1677
  // Every flush fails, so the record stays queued and the retry interval grows:
1190
1678
  // base (initial), base (1st retry), 2x, 4x, ...