@replayci/replay 0.1.3 → 0.1.5

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.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -127,7 +137,7 @@ var CaptureBuffer = class {
127
137
  }
128
138
  flush() {
129
139
  if (this.closed || this.remoteDisabled) {
130
- return Promise.resolve();
140
+ return Promise.resolve({ captured: 0, sent: 0, active: true, errors: [] });
131
141
  }
132
142
  if (this.flushPromise) {
133
143
  return this.flushPromise;
@@ -137,6 +147,7 @@ var CaptureBuffer = class {
137
147
  type: "flush_error",
138
148
  error: err instanceof Error ? err.message : String(err)
139
149
  });
150
+ return { captured: 0, sent: 0, active: true, errors: [err instanceof Error ? err.message : String(err)] };
140
151
  }).finally(() => {
141
152
  if (this.flushPromise === flushPromise) {
142
153
  this.flushPromise = void 0;
@@ -165,11 +176,11 @@ var CaptureBuffer = class {
165
176
  }
166
177
  async flushOnce() {
167
178
  if (this.closed || this.remoteDisabled || this.queue.length === 0) {
168
- return;
179
+ return { captured: 0, sent: 0, active: true, errors: [] };
169
180
  }
170
181
  const now = this.now();
171
182
  if (this.circuitOpenUntil > now) {
172
- return;
183
+ return { captured: 0, sent: 0, active: true, errors: [] };
173
184
  }
174
185
  if (this.circuitOpenUntil !== 0) {
175
186
  this.circuitOpenUntil = 0;
@@ -177,8 +188,9 @@ var CaptureBuffer = class {
177
188
  }
178
189
  const batch = this.queue.splice(0, Math.min(this.queue.length, MAX_BATCH_SIZE));
179
190
  if (batch.length === 0) {
180
- return;
191
+ return { captured: 0, sent: 0, active: true, errors: [] };
181
192
  }
193
+ const captured = batch.length;
182
194
  this.lastFlushAttemptMs = this.now();
183
195
  emitStateChange(this.onStateChange, { type: "flush_attempt" });
184
196
  let payload = "";
@@ -186,7 +198,7 @@ var CaptureBuffer = class {
186
198
  payload = JSON.stringify({ captures: batch });
187
199
  } catch {
188
200
  this.handleFailure("JSON serialization failed");
189
- return;
201
+ return { captured, sent: 0, active: true, errors: ["JSON serialization failed"] };
190
202
  }
191
203
  const controller = new AbortController();
192
204
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -209,19 +221,23 @@ var CaptureBuffer = class {
209
221
  this.circuitOpenUntil = Number.MAX_SAFE_INTEGER;
210
222
  emitDiagnostics(this.diagnostics, { type: "remote_disabled" });
211
223
  emitStateChange(this.onStateChange, { type: "remote_disabled" });
212
- return;
224
+ return { captured, sent: 0, active: true, errors: ["remote_disabled"] };
213
225
  }
214
226
  if (!response.ok) {
215
- this.handleFailure(`HTTP ${response.status}`);
216
- return;
227
+ const msg = `HTTP ${response.status}`;
228
+ this.handleFailure(msg);
229
+ return { captured, sent: 0, active: true, errors: [msg] };
217
230
  }
218
231
  this.failureCount = 0;
219
232
  this.circuitOpenUntil = 0;
220
233
  this.lastFlushSuccessMs = this.now();
221
234
  this.lastFlushErrorMsg = null;
222
235
  emitStateChange(this.onStateChange, { type: "flush_success", batch_size: batch.length });
236
+ return { captured, sent: captured, active: true, errors: [] };
223
237
  } catch (err) {
224
- this.handleFailure(err instanceof Error ? err.message : String(err));
238
+ const msg = err instanceof Error ? err.message : String(err);
239
+ this.handleFailure(msg);
240
+ return { captured, sent: 0, active: true, errors: [msg] };
225
241
  } finally {
226
242
  clearTimeout(timeout);
227
243
  }
@@ -1436,6 +1452,32 @@ function ensureDir(dir) {
1436
1452
  var REPLAY_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
1437
1453
  var DEFAULT_AGENT = "default";
1438
1454
  var IDLE_HEARTBEAT_MS = 3e4;
1455
+ function defaultDiagnosticsHandler(event) {
1456
+ switch (event.type) {
1457
+ case "observe_inactive":
1458
+ if (event.reason_code === "missing_api_key") {
1459
+ console.warn(
1460
+ "[replayci] No API key provided. observe() is inactive \u2014 calls will not be captured. Set REPLAYCI_API_KEY or pass apiKey to observe()."
1461
+ );
1462
+ }
1463
+ break;
1464
+ case "activation_warning":
1465
+ console.warn(`[replayci] ${event.message}`);
1466
+ break;
1467
+ case "flush_error":
1468
+ console.warn(`[replayci] flush failed: ${event.error}`);
1469
+ break;
1470
+ case "flush_empty":
1471
+ console.warn(
1472
+ "[replayci] flush(): No calls were captured. Ensure your LLM calls use the client returned by observe()."
1473
+ );
1474
+ break;
1475
+ case "circuit_open":
1476
+ break;
1477
+ default:
1478
+ break;
1479
+ }
1480
+ }
1439
1481
  function observe(client, opts = {}) {
1440
1482
  assertSupportedNodeRuntime();
1441
1483
  const sessionId = generateSessionId();
@@ -1445,38 +1487,45 @@ function observe(client, opts = {}) {
1445
1487
  if (isDisabled(opts)) {
1446
1488
  return createInactiveHandle(client, sessionId, agent, "disabled", void 0, now, opts.diagnostics, opts.stateDir);
1447
1489
  }
1490
+ const diagnosticsHandler = opts.diagnostics ?? defaultDiagnosticsHandler;
1448
1491
  const apiKey = resolveApiKey(opts);
1492
+ if (apiKey && !/^rci_(live|test)_/.test(apiKey)) {
1493
+ emitDiagnostic(diagnosticsHandler, {
1494
+ type: "activation_warning",
1495
+ message: "API key format looks wrong (expected 'rci_live_...' or 'rci_test_...'). Verify your key at app.replayci.com/settings."
1496
+ });
1497
+ }
1449
1498
  if (!apiKey) {
1450
- return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, opts.diagnostics, opts.stateDir);
1499
+ return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, diagnosticsHandler, opts.stateDir);
1451
1500
  }
1452
- const provider = detectProviderSafely(client, opts.diagnostics);
1501
+ const provider = detectProviderSafely(client, diagnosticsHandler);
1453
1502
  if (!provider) {
1454
- return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, opts.diagnostics, opts.stateDir);
1503
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, diagnosticsHandler, opts.stateDir);
1455
1504
  }
1456
1505
  const patchTarget = resolvePatchTarget(client, provider);
1457
1506
  if (!patchTarget) {
1458
- emitDiagnostic(opts.diagnostics, {
1507
+ emitDiagnostic(diagnosticsHandler, {
1459
1508
  type: "unsupported_client",
1460
1509
  mode: "observe",
1461
1510
  detail: `Unsupported ${provider} client shape.`
1462
1511
  });
1463
- return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, opts.diagnostics, opts.stateDir);
1512
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, diagnosticsHandler, opts.stateDir);
1464
1513
  }
1465
1514
  if (isWrapped(client, patchTarget.target)) {
1466
- emitDiagnostic(opts.diagnostics, {
1515
+ emitDiagnostic(diagnosticsHandler, {
1467
1516
  type: "double_wrap",
1468
1517
  mode: "observe"
1469
1518
  });
1470
- return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, opts.diagnostics, opts.stateDir);
1519
+ return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, diagnosticsHandler, opts.stateDir);
1471
1520
  }
1472
1521
  const patchabilityError = getPatchabilityError(patchTarget.target, patchTarget.methodName);
1473
1522
  if (patchabilityError) {
1474
- emitDiagnostic(opts.diagnostics, {
1523
+ emitDiagnostic(diagnosticsHandler, {
1475
1524
  type: "unsupported_client",
1476
1525
  mode: "observe",
1477
1526
  detail: patchabilityError
1478
1527
  });
1479
- return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, opts.diagnostics, opts.stateDir);
1528
+ return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, diagnosticsHandler, opts.stateDir);
1480
1529
  }
1481
1530
  const captureLevel = normalizeCaptureLevel(opts.captureLevel);
1482
1531
  const patchTargetName = `${provider}.${provider === "openai" ? "chat.completions.create" : "messages.create"}`;
@@ -1534,7 +1583,7 @@ function observe(client, opts = {}) {
1534
1583
  const detail = err instanceof Error ? err.message : "Failed to write health snapshot";
1535
1584
  lastHealthStoreErrorAt = (/* @__PURE__ */ new Date()).toISOString();
1536
1585
  lastHealthStoreError = detail;
1537
- emitDiagnostic(opts.diagnostics, {
1586
+ emitDiagnostic(diagnosticsHandler, {
1538
1587
  type: "health_store_error",
1539
1588
  detail
1540
1589
  });
@@ -1574,7 +1623,7 @@ function observe(client, opts = {}) {
1574
1623
  runtimeState.consecutive_failures = 0;
1575
1624
  runtimeState.circuit_open_until = null;
1576
1625
  runtimeState.queue_size = buffer.size;
1577
- emitDiagnostic(opts.diagnostics, {
1626
+ emitDiagnostic(diagnosticsHandler, {
1578
1627
  type: "flush_succeeded",
1579
1628
  batch_size: event.batch_size
1580
1629
  });
@@ -1610,7 +1659,7 @@ function observe(client, opts = {}) {
1610
1659
  maxBuffer: opts.maxBuffer,
1611
1660
  flushMs: opts.flushMs,
1612
1661
  timeoutMs: opts.timeoutMs,
1613
- diagnostics: opts.diagnostics,
1662
+ diagnostics: diagnosticsHandler,
1614
1663
  onStateChange: onBufferStateChange
1615
1664
  });
1616
1665
  registerBeforeExit(buffer);
@@ -1634,7 +1683,7 @@ function observe(client, opts = {}) {
1634
1683
  persistHealthEvent();
1635
1684
  safeRunJanitor(sessionsDir, sessionId);
1636
1685
  startHeartbeat();
1637
- emitDiagnostic(opts.diagnostics, {
1686
+ emitDiagnostic(diagnosticsHandler, {
1638
1687
  type: "observe_activated",
1639
1688
  session_id: sessionId,
1640
1689
  provider,
@@ -1657,7 +1706,7 @@ function observe(client, opts = {}) {
1657
1706
  sessionId,
1658
1707
  runtimeState,
1659
1708
  persistHealthEvent,
1660
- diagnostics: opts.diagnostics
1709
+ diagnostics: diagnosticsHandler
1661
1710
  });
1662
1711
  return response;
1663
1712
  });
@@ -1667,8 +1716,12 @@ function observe(client, opts = {}) {
1667
1716
  let restored = false;
1668
1717
  return {
1669
1718
  client,
1670
- flush() {
1671
- return buffer.flush();
1719
+ async flush() {
1720
+ const result = await buffer.flush();
1721
+ if (result.captured === 0 && result.errors.length === 0) {
1722
+ emitDiagnostic(diagnosticsHandler, { type: "flush_empty" });
1723
+ }
1724
+ return result;
1672
1725
  },
1673
1726
  restore() {
1674
1727
  if (restored) {
@@ -1707,7 +1760,7 @@ function observe(client, opts = {}) {
1707
1760
  };
1708
1761
  } catch (err) {
1709
1762
  const detail = err instanceof Error ? err.message : "Unknown internal error";
1710
- return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics, opts.stateDir);
1763
+ return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics ?? defaultDiagnosticsHandler, opts.stateDir);
1711
1764
  }
1712
1765
  }
1713
1766
  function createInactiveHandle(client, sessionId, agent, reasonCode, detail, activatedAt, diagnostics, stateDir) {
@@ -1734,7 +1787,7 @@ function createInactiveHandle(client, sessionId, agent, reasonCode, detail, acti
1734
1787
  return {
1735
1788
  client,
1736
1789
  flush() {
1737
- return Promise.resolve();
1790
+ return Promise.resolve({ captured: 0, sent: 0, active: false, errors: [] });
1738
1791
  },
1739
1792
  restore() {
1740
1793
  },
@@ -2095,7 +2148,7 @@ function isTruthyEnvFlag(value) {
2095
2148
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
2096
2149
  }
2097
2150
  function normalizeCaptureLevel(level) {
2098
- return level === "metadata" || level === "full" ? level : "redacted";
2151
+ return level === "metadata" || level === "redacted" ? level : "full";
2099
2152
  }
2100
2153
  function emitDiagnostic(diagnostics, event) {
2101
2154
  try {
@@ -2126,6 +2179,14 @@ var import_contracts_core3 = require("@replayci/contracts-core");
2126
2179
  var import_contracts_core2 = require("@replayci/contracts-core");
2127
2180
  var import_node_fs2 = require("fs");
2128
2181
  var import_node_path2 = require("path");
2182
+
2183
+ // src/safeRegex.ts
2184
+ var import_re2 = __toESM(require("re2"), 1);
2185
+ function safeRegex(pattern) {
2186
+ return new import_re2.default(pattern);
2187
+ }
2188
+
2189
+ // src/contracts.ts
2129
2190
  var CONTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2130
2191
  var MAX_REGEX_BYTES = 1024;
2131
2192
  var NESTED_QUANTIFIER_RE = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)(?:[+*]|\{\d+(?:,\d*)?\})/;
@@ -2319,7 +2380,7 @@ function validateSafeRegexes(contract) {
2319
2380
  );
2320
2381
  }
2321
2382
  try {
2322
- void new RegExp(invariant.regex);
2383
+ void safeRegex(invariant.regex);
2323
2384
  } catch (error) {
2324
2385
  throw new ReplayConfigurationError(
2325
2386
  `Invalid regex in ${contractLabel} (${group.label}, ${invariant.path}): ${formatErrorMessage(error)}`
package/dist/index.d.cts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { Contract } from '@replayci/contracts-core';
2
2
 
3
+ interface FlushResult {
4
+ /** Number of captures in the buffer when flush was called */
5
+ captured: number;
6
+ /** Number of captures successfully sent to the server */
7
+ sent: number;
8
+ /** Whether the handle is active (false = no apiKey or disabled) */
9
+ active: boolean;
10
+ /** Error messages from send attempts */
11
+ errors: string[];
12
+ }
3
13
  type ContractFailure = {
4
14
  path: string;
5
15
  operator: string;
@@ -93,6 +103,11 @@ type ObserveDiagnosticEvent = {
93
103
  type: "unsupported_client";
94
104
  mode: "observe";
95
105
  detail: string;
106
+ } | {
107
+ type: "activation_warning";
108
+ message: string;
109
+ } | {
110
+ type: "flush_empty";
96
111
  };
97
112
  type ObserveOptions = {
98
113
  agent?: string;
@@ -109,7 +124,7 @@ type ObserveOptions = {
109
124
  };
110
125
  type ObserveHandle<T> = {
111
126
  client: T;
112
- flush: () => Promise<void>;
127
+ flush: () => Promise<FlushResult>;
113
128
  restore: () => void;
114
129
  getHealth: () => ObserveHealthSnapshot;
115
130
  };
@@ -123,4 +138,4 @@ type ValidateOptions = {
123
138
  declare function prepareContracts(input: string | string[] | Contract | Contract[]): Contract[];
124
139
  declare function validate(response: unknown, opts?: ValidateOptions): ValidationResult;
125
140
 
126
- export { type ContractFailure, type ObserveActivationReasonCode, type ObserveDiagnosticEvent, type ObserveHandle, type ObserveHealthSnapshot, type ObserveOptions, type ObserveSessionState, type ValidationResult, observe, prepareContracts, validate };
141
+ export { type ContractFailure, type FlushResult, type ObserveActivationReasonCode, type ObserveDiagnosticEvent, type ObserveHandle, type ObserveHealthSnapshot, type ObserveOptions, type ObserveSessionState, type ValidationResult, observe, prepareContracts, validate };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { Contract } from '@replayci/contracts-core';
2
2
 
3
+ interface FlushResult {
4
+ /** Number of captures in the buffer when flush was called */
5
+ captured: number;
6
+ /** Number of captures successfully sent to the server */
7
+ sent: number;
8
+ /** Whether the handle is active (false = no apiKey or disabled) */
9
+ active: boolean;
10
+ /** Error messages from send attempts */
11
+ errors: string[];
12
+ }
3
13
  type ContractFailure = {
4
14
  path: string;
5
15
  operator: string;
@@ -93,6 +103,11 @@ type ObserveDiagnosticEvent = {
93
103
  type: "unsupported_client";
94
104
  mode: "observe";
95
105
  detail: string;
106
+ } | {
107
+ type: "activation_warning";
108
+ message: string;
109
+ } | {
110
+ type: "flush_empty";
96
111
  };
97
112
  type ObserveOptions = {
98
113
  agent?: string;
@@ -109,7 +124,7 @@ type ObserveOptions = {
109
124
  };
110
125
  type ObserveHandle<T> = {
111
126
  client: T;
112
- flush: () => Promise<void>;
127
+ flush: () => Promise<FlushResult>;
113
128
  restore: () => void;
114
129
  getHealth: () => ObserveHealthSnapshot;
115
130
  };
@@ -123,4 +138,4 @@ type ValidateOptions = {
123
138
  declare function prepareContracts(input: string | string[] | Contract | Contract[]): Contract[];
124
139
  declare function validate(response: unknown, opts?: ValidateOptions): ValidationResult;
125
140
 
126
- export { type ContractFailure, type ObserveActivationReasonCode, type ObserveDiagnosticEvent, type ObserveHandle, type ObserveHealthSnapshot, type ObserveOptions, type ObserveSessionState, type ValidationResult, observe, prepareContracts, validate };
141
+ export { type ContractFailure, type FlushResult, type ObserveActivationReasonCode, type ObserveDiagnosticEvent, type ObserveHandle, type ObserveHealthSnapshot, type ObserveOptions, type ObserveSessionState, type ValidationResult, observe, prepareContracts, validate };
package/dist/index.js CHANGED
@@ -99,7 +99,7 @@ var CaptureBuffer = class {
99
99
  }
100
100
  flush() {
101
101
  if (this.closed || this.remoteDisabled) {
102
- return Promise.resolve();
102
+ return Promise.resolve({ captured: 0, sent: 0, active: true, errors: [] });
103
103
  }
104
104
  if (this.flushPromise) {
105
105
  return this.flushPromise;
@@ -109,6 +109,7 @@ var CaptureBuffer = class {
109
109
  type: "flush_error",
110
110
  error: err instanceof Error ? err.message : String(err)
111
111
  });
112
+ return { captured: 0, sent: 0, active: true, errors: [err instanceof Error ? err.message : String(err)] };
112
113
  }).finally(() => {
113
114
  if (this.flushPromise === flushPromise) {
114
115
  this.flushPromise = void 0;
@@ -137,11 +138,11 @@ var CaptureBuffer = class {
137
138
  }
138
139
  async flushOnce() {
139
140
  if (this.closed || this.remoteDisabled || this.queue.length === 0) {
140
- return;
141
+ return { captured: 0, sent: 0, active: true, errors: [] };
141
142
  }
142
143
  const now = this.now();
143
144
  if (this.circuitOpenUntil > now) {
144
- return;
145
+ return { captured: 0, sent: 0, active: true, errors: [] };
145
146
  }
146
147
  if (this.circuitOpenUntil !== 0) {
147
148
  this.circuitOpenUntil = 0;
@@ -149,8 +150,9 @@ var CaptureBuffer = class {
149
150
  }
150
151
  const batch = this.queue.splice(0, Math.min(this.queue.length, MAX_BATCH_SIZE));
151
152
  if (batch.length === 0) {
152
- return;
153
+ return { captured: 0, sent: 0, active: true, errors: [] };
153
154
  }
155
+ const captured = batch.length;
154
156
  this.lastFlushAttemptMs = this.now();
155
157
  emitStateChange(this.onStateChange, { type: "flush_attempt" });
156
158
  let payload = "";
@@ -158,7 +160,7 @@ var CaptureBuffer = class {
158
160
  payload = JSON.stringify({ captures: batch });
159
161
  } catch {
160
162
  this.handleFailure("JSON serialization failed");
161
- return;
163
+ return { captured, sent: 0, active: true, errors: ["JSON serialization failed"] };
162
164
  }
163
165
  const controller = new AbortController();
164
166
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -181,19 +183,23 @@ var CaptureBuffer = class {
181
183
  this.circuitOpenUntil = Number.MAX_SAFE_INTEGER;
182
184
  emitDiagnostics(this.diagnostics, { type: "remote_disabled" });
183
185
  emitStateChange(this.onStateChange, { type: "remote_disabled" });
184
- return;
186
+ return { captured, sent: 0, active: true, errors: ["remote_disabled"] };
185
187
  }
186
188
  if (!response.ok) {
187
- this.handleFailure(`HTTP ${response.status}`);
188
- return;
189
+ const msg = `HTTP ${response.status}`;
190
+ this.handleFailure(msg);
191
+ return { captured, sent: 0, active: true, errors: [msg] };
189
192
  }
190
193
  this.failureCount = 0;
191
194
  this.circuitOpenUntil = 0;
192
195
  this.lastFlushSuccessMs = this.now();
193
196
  this.lastFlushErrorMsg = null;
194
197
  emitStateChange(this.onStateChange, { type: "flush_success", batch_size: batch.length });
198
+ return { captured, sent: captured, active: true, errors: [] };
195
199
  } catch (err) {
196
- this.handleFailure(err instanceof Error ? err.message : String(err));
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ this.handleFailure(msg);
202
+ return { captured, sent: 0, active: true, errors: [msg] };
197
203
  } finally {
198
204
  clearTimeout(timeout);
199
205
  }
@@ -1418,6 +1424,32 @@ function ensureDir(dir) {
1418
1424
  var REPLAY_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
1419
1425
  var DEFAULT_AGENT = "default";
1420
1426
  var IDLE_HEARTBEAT_MS = 3e4;
1427
+ function defaultDiagnosticsHandler(event) {
1428
+ switch (event.type) {
1429
+ case "observe_inactive":
1430
+ if (event.reason_code === "missing_api_key") {
1431
+ console.warn(
1432
+ "[replayci] No API key provided. observe() is inactive \u2014 calls will not be captured. Set REPLAYCI_API_KEY or pass apiKey to observe()."
1433
+ );
1434
+ }
1435
+ break;
1436
+ case "activation_warning":
1437
+ console.warn(`[replayci] ${event.message}`);
1438
+ break;
1439
+ case "flush_error":
1440
+ console.warn(`[replayci] flush failed: ${event.error}`);
1441
+ break;
1442
+ case "flush_empty":
1443
+ console.warn(
1444
+ "[replayci] flush(): No calls were captured. Ensure your LLM calls use the client returned by observe()."
1445
+ );
1446
+ break;
1447
+ case "circuit_open":
1448
+ break;
1449
+ default:
1450
+ break;
1451
+ }
1452
+ }
1421
1453
  function observe(client, opts = {}) {
1422
1454
  assertSupportedNodeRuntime();
1423
1455
  const sessionId = generateSessionId();
@@ -1427,38 +1459,45 @@ function observe(client, opts = {}) {
1427
1459
  if (isDisabled(opts)) {
1428
1460
  return createInactiveHandle(client, sessionId, agent, "disabled", void 0, now, opts.diagnostics, opts.stateDir);
1429
1461
  }
1462
+ const diagnosticsHandler = opts.diagnostics ?? defaultDiagnosticsHandler;
1430
1463
  const apiKey = resolveApiKey(opts);
1464
+ if (apiKey && !/^rci_(live|test)_/.test(apiKey)) {
1465
+ emitDiagnostic(diagnosticsHandler, {
1466
+ type: "activation_warning",
1467
+ message: "API key format looks wrong (expected 'rci_live_...' or 'rci_test_...'). Verify your key at app.replayci.com/settings."
1468
+ });
1469
+ }
1431
1470
  if (!apiKey) {
1432
- return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, opts.diagnostics, opts.stateDir);
1471
+ return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, diagnosticsHandler, opts.stateDir);
1433
1472
  }
1434
- const provider = detectProviderSafely(client, opts.diagnostics);
1473
+ const provider = detectProviderSafely(client, diagnosticsHandler);
1435
1474
  if (!provider) {
1436
- return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, opts.diagnostics, opts.stateDir);
1475
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, diagnosticsHandler, opts.stateDir);
1437
1476
  }
1438
1477
  const patchTarget = resolvePatchTarget(client, provider);
1439
1478
  if (!patchTarget) {
1440
- emitDiagnostic(opts.diagnostics, {
1479
+ emitDiagnostic(diagnosticsHandler, {
1441
1480
  type: "unsupported_client",
1442
1481
  mode: "observe",
1443
1482
  detail: `Unsupported ${provider} client shape.`
1444
1483
  });
1445
- return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, opts.diagnostics, opts.stateDir);
1484
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, diagnosticsHandler, opts.stateDir);
1446
1485
  }
1447
1486
  if (isWrapped(client, patchTarget.target)) {
1448
- emitDiagnostic(opts.diagnostics, {
1487
+ emitDiagnostic(diagnosticsHandler, {
1449
1488
  type: "double_wrap",
1450
1489
  mode: "observe"
1451
1490
  });
1452
- return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, opts.diagnostics, opts.stateDir);
1491
+ return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, diagnosticsHandler, opts.stateDir);
1453
1492
  }
1454
1493
  const patchabilityError = getPatchabilityError(patchTarget.target, patchTarget.methodName);
1455
1494
  if (patchabilityError) {
1456
- emitDiagnostic(opts.diagnostics, {
1495
+ emitDiagnostic(diagnosticsHandler, {
1457
1496
  type: "unsupported_client",
1458
1497
  mode: "observe",
1459
1498
  detail: patchabilityError
1460
1499
  });
1461
- return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, opts.diagnostics, opts.stateDir);
1500
+ return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, diagnosticsHandler, opts.stateDir);
1462
1501
  }
1463
1502
  const captureLevel = normalizeCaptureLevel(opts.captureLevel);
1464
1503
  const patchTargetName = `${provider}.${provider === "openai" ? "chat.completions.create" : "messages.create"}`;
@@ -1516,7 +1555,7 @@ function observe(client, opts = {}) {
1516
1555
  const detail = err instanceof Error ? err.message : "Failed to write health snapshot";
1517
1556
  lastHealthStoreErrorAt = (/* @__PURE__ */ new Date()).toISOString();
1518
1557
  lastHealthStoreError = detail;
1519
- emitDiagnostic(opts.diagnostics, {
1558
+ emitDiagnostic(diagnosticsHandler, {
1520
1559
  type: "health_store_error",
1521
1560
  detail
1522
1561
  });
@@ -1556,7 +1595,7 @@ function observe(client, opts = {}) {
1556
1595
  runtimeState.consecutive_failures = 0;
1557
1596
  runtimeState.circuit_open_until = null;
1558
1597
  runtimeState.queue_size = buffer.size;
1559
- emitDiagnostic(opts.diagnostics, {
1598
+ emitDiagnostic(diagnosticsHandler, {
1560
1599
  type: "flush_succeeded",
1561
1600
  batch_size: event.batch_size
1562
1601
  });
@@ -1592,7 +1631,7 @@ function observe(client, opts = {}) {
1592
1631
  maxBuffer: opts.maxBuffer,
1593
1632
  flushMs: opts.flushMs,
1594
1633
  timeoutMs: opts.timeoutMs,
1595
- diagnostics: opts.diagnostics,
1634
+ diagnostics: diagnosticsHandler,
1596
1635
  onStateChange: onBufferStateChange
1597
1636
  });
1598
1637
  registerBeforeExit(buffer);
@@ -1616,7 +1655,7 @@ function observe(client, opts = {}) {
1616
1655
  persistHealthEvent();
1617
1656
  safeRunJanitor(sessionsDir, sessionId);
1618
1657
  startHeartbeat();
1619
- emitDiagnostic(opts.diagnostics, {
1658
+ emitDiagnostic(diagnosticsHandler, {
1620
1659
  type: "observe_activated",
1621
1660
  session_id: sessionId,
1622
1661
  provider,
@@ -1639,7 +1678,7 @@ function observe(client, opts = {}) {
1639
1678
  sessionId,
1640
1679
  runtimeState,
1641
1680
  persistHealthEvent,
1642
- diagnostics: opts.diagnostics
1681
+ diagnostics: diagnosticsHandler
1643
1682
  });
1644
1683
  return response;
1645
1684
  });
@@ -1649,8 +1688,12 @@ function observe(client, opts = {}) {
1649
1688
  let restored = false;
1650
1689
  return {
1651
1690
  client,
1652
- flush() {
1653
- return buffer.flush();
1691
+ async flush() {
1692
+ const result = await buffer.flush();
1693
+ if (result.captured === 0 && result.errors.length === 0) {
1694
+ emitDiagnostic(diagnosticsHandler, { type: "flush_empty" });
1695
+ }
1696
+ return result;
1654
1697
  },
1655
1698
  restore() {
1656
1699
  if (restored) {
@@ -1689,7 +1732,7 @@ function observe(client, opts = {}) {
1689
1732
  };
1690
1733
  } catch (err) {
1691
1734
  const detail = err instanceof Error ? err.message : "Unknown internal error";
1692
- return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics, opts.stateDir);
1735
+ return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics ?? defaultDiagnosticsHandler, opts.stateDir);
1693
1736
  }
1694
1737
  }
1695
1738
  function createInactiveHandle(client, sessionId, agent, reasonCode, detail, activatedAt, diagnostics, stateDir) {
@@ -1716,7 +1759,7 @@ function createInactiveHandle(client, sessionId, agent, reasonCode, detail, acti
1716
1759
  return {
1717
1760
  client,
1718
1761
  flush() {
1719
- return Promise.resolve();
1762
+ return Promise.resolve({ captured: 0, sent: 0, active: false, errors: [] });
1720
1763
  },
1721
1764
  restore() {
1722
1765
  },
@@ -2077,7 +2120,7 @@ function isTruthyEnvFlag(value) {
2077
2120
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
2078
2121
  }
2079
2122
  function normalizeCaptureLevel(level) {
2080
- return level === "metadata" || level === "full" ? level : "redacted";
2123
+ return level === "metadata" || level === "redacted" ? level : "full";
2081
2124
  }
2082
2125
  function emitDiagnostic(diagnostics, event) {
2083
2126
  try {
@@ -2124,6 +2167,14 @@ import {
2124
2167
  relative,
2125
2168
  resolve
2126
2169
  } from "path";
2170
+
2171
+ // src/safeRegex.ts
2172
+ import RE2 from "re2";
2173
+ function safeRegex(pattern) {
2174
+ return new RE2(pattern);
2175
+ }
2176
+
2177
+ // src/contracts.ts
2127
2178
  var CONTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2128
2179
  var MAX_REGEX_BYTES = 1024;
2129
2180
  var NESTED_QUANTIFIER_RE = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)(?:[+*]|\{\d+(?:,\d*)?\})/;
@@ -2317,7 +2368,7 @@ function validateSafeRegexes(contract) {
2317
2368
  );
2318
2369
  }
2319
2370
  try {
2320
- void new RegExp(invariant.regex);
2371
+ void safeRegex(invariant.regex);
2321
2372
  } catch (error) {
2322
2373
  throw new ReplayConfigurationError(
2323
2374
  `Invalid regex in ${contractLabel} (${group.label}, ${invariant.path}): ${formatErrorMessage(error)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayci/replay",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "ReplayCI SDK for deterministic tool-call validation and observation.",
5
5
  "license": "ISC",
6
6
  "author": "ReplayCI",
@@ -51,6 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@replayci/contracts-core": "^0.1.0",
54
+ "re2": "^1.20.0",
54
55
  "yaml": "^2.0.0"
55
56
  },
56
57
  "peerDependencies": {
@@ -65,9 +66,6 @@
65
66
  "optional": true
66
67
  }
67
68
  },
68
- "optionalDependencies": {
69
- "re2": "^1.20.0"
70
- },
71
69
  "devDependencies": {
72
70
  "tsup": "^8.5.0",
73
71
  "typescript": "^5.9.3",