autotel-subscribers 33.0.0 → 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/event-subscriber-base.ts","../src/file.ts"],"names":[],"mappings":"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","file":"file.js","sourcesContent":["/**\n * EventSubscriber - Standard base class for building custom subscribers\n *\n * This is the recommended base class for creating custom events subscribers.\n * It provides production-ready features out of the box:\n *\n * **Built-in Features:**\n * - **Error Handling**: Automatic error catching with customizable handlers\n * - **Pending Request Tracking**: Ensures all requests complete during shutdown\n * - **Graceful Shutdown**: Drains pending requests before closing\n * - **Enable/Disable**: Runtime control to turn subscriber on/off\n * - **Normalized Payload**: Consistent event structure across all event types\n *\n * **When to use:**\n * - Building custom subscribers for any platform\n * - Production deployments requiring reliability\n * - Need graceful shutdown and error handling\n *\n * @example Basic usage\n * ```typescript\n * import { EventSubscriber, EventPayload } from 'autotel-subscribers';\n *\n * class SnowflakeSubscriber extends EventSubscriber {\n * name = 'SnowflakeSubscriber';\n * version = '1.0.0';\n *\n * protected async sendToDestination(payload: EventPayload): Promise<void> {\n * await snowflakeClient.execute(\n * `INSERT INTO events VALUES (?, ?, ?)`,\n * [payload.type, payload.name, JSON.stringify(payload.attributes)]\n * );\n * }\n * }\n * ```\n *\n * @example With buffering\n * ```typescript\n * class BufferedSubscriber extends EventSubscriber {\n * name = 'BufferedSubscriber';\n * private buffer: EventPayload[] = [];\n *\n * protected async sendToDestination(payload: EventPayload): Promise<void> {\n * this.buffer.push(payload);\n *\n * if (this.buffer.length >= 100) {\n * await this.flush();\n * }\n * }\n *\n * async shutdown(): Promise<void> {\n * await super.shutdown(); // Drain pending requests first\n * await this.flush(); // Then flush buffer\n * }\n *\n * private async flush(): Promise<void> {\n * if (this.buffer.length === 0) return;\n *\n * const batch = [...this.buffer];\n * this.buffer = [];\n *\n * await apiClient.sendBatch(batch);\n * }\n * }\n * ```\n */\n\nimport type {\n EventSubscriber as IEventSubscriber,\n EventAttributes,\n EventAttributesInput,\n FunnelStatus,\n OutcomeStatus,\n AutotelEventContext,\n EventTrackingOptions,\n} from 'autotel/event-subscriber';\n\n// Re-export types for convenience\nexport type { AutotelEventContext, EventTrackingOptions } from 'autotel/event-subscriber';\n\n/**\n * Payload sent to destination\n */\nexport interface EventPayload {\n /** Event type: 'event', 'funnel', 'outcome', or 'value' */\n type: 'event' | 'funnel' | 'outcome' | 'value';\n\n /** Event name or metric name */\n name: string;\n\n /** Optional attributes */\n attributes?: EventAttributes;\n\n /** For funnel events: funnel name */\n funnel?: string;\n\n /** For funnel events: step status (from FunnelStatus enum) */\n step?: FunnelStatus | string;\n\n /** For funnel events: custom step name (from trackFunnelProgression) */\n stepName?: string;\n\n /** For funnel events: numeric position in funnel */\n stepNumber?: number;\n\n /** For outcome events: operation name */\n operation?: string;\n\n /** For outcome events: outcome status */\n outcome?: OutcomeStatus;\n\n /** For value events: numeric value */\n value?: number;\n\n /** Timestamp (ISO 8601) */\n timestamp: string;\n\n /**\n * Autotel trace context (present when events.includeTraceContext is enabled)\n *\n * Subscribers should map these to platform-specific field names:\n * - PostHog: autotel.trace_id → $trace_id\n * - Mixpanel: autotel.trace_id → trace_id\n */\n autotel?: AutotelEventContext;\n /** Optional schema metadata for contract-aware subscribers. */\n schema?: EventTrackingOptions['schema'];\n}\n\n/**\n * Standard base class for building custom events subscribers\n *\n * **What it provides:**\n * - Consistent payload structure (normalized across all event types)\n * - Enable/disable flag (runtime control)\n * - Automatic error handling (with customizable error handlers)\n * - Pending requests tracking (ensures no lost events during shutdown)\n * - Graceful shutdown (drains pending requests before closing)\n *\n * **Usage:**\n * Extend this class and implement `sendToDestination()`. All other methods\n * (trackEvent, trackFunnelStep, trackOutcome, trackValue, shutdown) are handled automatically.\n *\n * For high-throughput streaming platforms (Kafka, Kinesis, Pub/Sub), use `StreamingEventSubscriber` instead.\n */\nexport abstract class EventSubscriber implements IEventSubscriber {\n /**\n * Subscriber name (required for debugging)\n */\n abstract readonly name: string;\n\n /**\n * Subscriber version (optional)\n */\n readonly version?: string;\n\n /**\n * Enable/disable the subscriber (default: true)\n */\n protected enabled: boolean = true;\n\n /**\n * Track pending requests for graceful shutdown\n */\n private pendingRequests: Set<Promise<void>> = new Set();\n\n /**\n * Send payload to destination\n *\n * Override this method to implement your destination-specific logic.\n * This is called for all event types (event, funnel, outcome, value).\n *\n * @param payload - Normalized event payload\n */\n protected abstract sendToDestination(payload: EventPayload): Promise<void>;\n\n /**\n * Optional: Handle errors\n *\n * Override this to customize error handling (logging, retries, etc.).\n * Default behavior: log to console.error\n *\n * @param error - Error that occurred\n * @param payload - Event payload that failed\n */\n protected handleError(error: Error, payload: EventPayload): void {\n console.error(\n `[${this.name}] Failed to send ${payload.type}:`,\n error,\n payload,\n );\n }\n\n /**\n * Filter out undefined and null values from attributes\n *\n * This improves DX by allowing callers to pass objects with optional properties\n * without having to manually filter them first.\n *\n * @param attributes - Input attributes (may contain undefined/null)\n * @returns Filtered attributes with only defined values, or undefined if empty\n *\n * @example\n * ```typescript\n * const filtered = this.filterAttributes({\n * userId: user.id,\n * email: user.email, // might be undefined\n * plan: null, // will be filtered out\n * });\n * // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }\n * ```\n */\n protected filterAttributes(\n attributes?: EventAttributesInput,\n ): EventAttributes | undefined {\n if (!attributes) return undefined;\n\n const filtered: EventAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined && value !== null) {\n filtered[key] = value;\n }\n }\n\n // Return undefined if no attributes remain after filtering\n return Object.keys(filtered).length > 0 ? filtered : undefined;\n }\n\n /**\n * Track an event\n */\n async trackEvent(\n name: string,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'event',\n name,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\n schema: options?.schema,\n };\n\n await this.send(payload);\n }\n\n /**\n * Track a funnel step\n */\n async trackFunnelStep(\n funnelName: string,\n step: FunnelStatus,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'funnel',\n name: `${funnelName}.${step}`,\n funnel: funnelName,\n step,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\n };\n\n await this.send(payload);\n }\n\n /**\n * Track an outcome\n */\n async trackOutcome(\n operationName: string,\n outcome: OutcomeStatus,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'outcome',\n name: `${operationName}.${outcome}`,\n operation: operationName,\n outcome,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\n };\n\n await this.send(payload);\n }\n\n /**\n * Track a value/metric\n */\n async trackValue(\n name: string,\n value: number,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'value',\n name,\n value,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\n };\n\n await this.send(payload);\n }\n\n /**\n * Track funnel progression with custom step names\n *\n * Unlike trackFunnelStep which uses FunnelStatus enum values,\n * this method allows any string as the step name for flexible funnel tracking.\n *\n * @param funnelName - Name of the funnel (e.g., \"checkout\", \"onboarding\")\n * @param stepName - Custom step name (e.g., \"cart_viewed\", \"payment_entered\")\n * @param stepNumber - Optional numeric position in the funnel\n * @param attributes - Optional event attributes\n * @param options - Optional tracking options including autotel context\n */\n async trackFunnelProgression(\n funnelName: string,\n stepName: string,\n stepNumber?: number,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'funnel',\n name: `${funnelName}.${stepName}`,\n funnel: funnelName,\n step: stepName,\n stepName,\n stepNumber,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\n };\n\n await this.send(payload);\n }\n\n /**\n * Flush pending requests and clean up\n *\n * CRITICAL: Prevents race condition during shutdown\n * 1. Disables subscriber to stop new events\n * 2. Drains all pending requests (with retry logic)\n * 3. Ensures flush guarantee\n *\n * Override this if you need custom cleanup logic (close connections, flush buffers, etc.),\n * but ALWAYS call super.shutdown() first to drain pending requests.\n */\n async shutdown(): Promise<void> {\n // 1. Stop accepting new events (prevents race condition)\n this.enabled = false;\n\n // 2. Drain pending requests with retry logic\n // Loop until empty to handle race where new requests added during Promise.allSettled\n const maxDrainAttempts = 10;\n const drainIntervalMs = 50;\n\n for (let attempt = 0; attempt < maxDrainAttempts; attempt++) {\n if (this.pendingRequests.size === 0) {\n break;\n }\n\n // Wait for current batch\n await Promise.allSettled(this.pendingRequests);\n\n // Small delay to catch any stragglers added during allSettled\n if (this.pendingRequests.size > 0 && attempt < maxDrainAttempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, drainIntervalMs));\n }\n }\n\n // 3. Warn if we still have pending requests (shouldn't happen, but be defensive)\n if (this.pendingRequests.size > 0) {\n console.warn(\n `[${this.name}] Shutdown completed with ${this.pendingRequests.size} pending requests still in-flight. ` +\n `This may indicate a bug in the subscriber or extremely slow destination.`\n );\n }\n }\n\n /**\n * Internal: Send payload and track request\n */\n private async send(payload: EventPayload): Promise<void> {\n const request = this.sendWithErrorHandling(payload);\n this.pendingRequests.add(request);\n\n void request.finally(() => {\n this.pendingRequests.delete(request);\n });\n\n return request;\n }\n\n /**\n * Internal: Send with error handling\n */\n private async sendWithErrorHandling(\n payload: EventPayload,\n ): Promise<void> {\n try {\n await this.sendToDestination(payload);\n } catch (error) {\n this.handleError(error as Error, payload);\n }\n }\n}\n\nexport {\n type EventAttributes,\n type EventAttributesInput,\n type FunnelStatus,\n type OutcomeStatus,\n} from 'autotel/event-subscriber';\n","/**\n * File subscriber for autotel.\n *\n * Appends each tracked event to a file as newline-delimited JSON (NDJSON).\n * Useful for AI agents, scripts, evals, and local debugging that want\n * structured events on disk without a hosted backend. Query the file with\n * `jq`, load it into a notebook, or feed it to an agent.\n *\n * @example\n * ```typescript\n * import { Event } from 'autotel/events';\n * import { FileSubscriber } from 'autotel-subscribers/file';\n *\n * const events = new Event('worker', {\n * subscribers: [new FileSubscriber({ path: './telemetry/events.ndjson' })],\n * });\n * ```\n */\n\nimport { appendFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { EventSubscriber, type EventPayload } from './event-subscriber-base';\n\nexport interface FileSubscriberConfig {\n /** File path to append newline-delimited JSON events to. */\n path: string;\n /** Enable or disable the subscriber. Default `true`. */\n enabled?: boolean;\n /** Pretty-print each event as indented JSON instead of one line. Default `false`. */\n pretty?: boolean;\n /** Create parent directories if they do not exist. Default `true`. */\n mkdir?: boolean;\n /**\n * Transform a payload before writing. Return `null` to skip the event.\n * Defaults to writing the normalized payload unchanged.\n */\n transform?: (payload: EventPayload) => Record<string, unknown> | null;\n}\n\nexport class FileSubscriber extends EventSubscriber {\n readonly name = 'FileSubscriber';\n readonly version = '1.0.0';\n\n private readonly filePath: string;\n private readonly pretty: boolean;\n private readonly ensureDir: boolean;\n private readonly transform?: (\n payload: EventPayload,\n ) => Record<string, unknown> | null;\n\n /** Serializes writes so concurrent events never interleave on disk. */\n private writeChain: Promise<void> = Promise.resolve();\n private dirEnsured = false;\n\n constructor(config: FileSubscriberConfig) {\n super();\n this.filePath = config.path;\n this.enabled = config.enabled ?? true;\n this.pretty = config.pretty ?? false;\n this.ensureDir = config.mkdir ?? true;\n this.transform = config.transform;\n }\n\n protected async sendToDestination(payload: EventPayload): Promise<void> {\n if (!this.enabled) return;\n\n const record = this.transform ? this.transform(payload) : payload;\n if (record === null) return;\n\n const json = this.pretty\n ? JSON.stringify(record, null, 2)\n : JSON.stringify(record);\n const line = `${json}\\n`;\n\n const run = this.writeChain.then(() => this.write(line));\n // Keep the chain ordered and alive even if one write rejects; the failed\n // write still rejects `run` so the base class can report it.\n this.writeChain = run.catch(() => {});\n await run;\n }\n\n private async write(line: string): Promise<void> {\n if (this.ensureDir && !this.dirEnsured) {\n const dir = path.dirname(this.filePath);\n if (dir && dir !== '.') {\n await mkdir(dir, { recursive: true });\n }\n this.dirEnsured = true;\n }\n await appendFile(this.filePath, line, 'utf8');\n }\n\n override async shutdown(): Promise<void> {\n await this.writeChain;\n await super.shutdown();\n }\n}\n"]}
package/dist/index.cjs CHANGED
@@ -110,8 +110,8 @@ function createGetModuleFromFilename(basePath = process.argv[1] ? path.dirname(p
110
110
  return decodedFile;
111
111
  };
112
112
  }
113
- function normalizeWindowsPath(path2) {
114
- return path2.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
113
+ function normalizeWindowsPath(path3) {
114
+ return path3.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
115
115
  }
116
116
  var init_module_node = __esm({
117
117
  "../../node_modules/.pnpm/posthog-node@5.34.1_rxjs@7.8.2/node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs"() {
@@ -2729,9 +2729,9 @@ async function addSourceContext(frames) {
2729
2729
  LRU_FILE_CONTENTS_CACHE.reduce();
2730
2730
  return frames;
2731
2731
  }
2732
- function getContextLinesFromFile(path2, ranges, output) {
2732
+ function getContextLinesFromFile(path3, ranges, output) {
2733
2733
  return new Promise((resolve) => {
2734
- const stream = fs$1.createReadStream(path2);
2734
+ const stream = fs$1.createReadStream(path3);
2735
2735
  const lineReaded = readline.createInterface({
2736
2736
  input: stream
2737
2737
  });
@@ -2746,7 +2746,7 @@ function getContextLinesFromFile(path2, ranges, output) {
2746
2746
  let rangeStart = range[0];
2747
2747
  let rangeEnd = range[1];
2748
2748
  function onStreamError() {
2749
- LRU_FILE_CONTENTS_FS_READ_FAILED.set(path2, 1);
2749
+ LRU_FILE_CONTENTS_FS_READ_FAILED.set(path3, 1);
2750
2750
  lineReaded.close();
2751
2751
  lineReaded.removeAllListeners();
2752
2752
  destroyStreamAndResolve();
@@ -2807,8 +2807,8 @@ function clearLineContext(frame) {
2807
2807
  delete frame.context_line;
2808
2808
  delete frame.post_context;
2809
2809
  }
2810
- function shouldSkipContextLinesForFile(path2) {
2811
- return path2.startsWith("node:") || path2.endsWith(".min.js") || path2.endsWith(".min.cjs") || path2.endsWith(".min.mjs") || path2.startsWith("data:");
2810
+ function shouldSkipContextLinesForFile(path3) {
2811
+ return path3.startsWith("node:") || path3.endsWith(".min.js") || path3.endsWith(".min.cjs") || path3.endsWith(".min.mjs") || path3.startsWith("data:");
2812
2812
  }
2813
2813
  function shouldSkipContextLinesForFrame(frame) {
2814
2814
  if (void 0 !== frame.lineno && frame.lineno > MAX_CONTEXTLINES_LINENO) return true;
@@ -7295,11 +7295,11 @@ var require_flags = __commonJS({
7295
7295
  params.append(key, value);
7296
7296
  }
7297
7297
  }
7298
- const path2 = `${this.endpoint}?${params.toString()}`;
7298
+ const path3 = `${this.endpoint}?${params.toString()}`;
7299
7299
  const requestOptions = {
7300
7300
  host: this.providerConfig.api_host,
7301
7301
  port: 443,
7302
- path: path2,
7302
+ path: path3,
7303
7303
  method: "GET",
7304
7304
  headers: {
7305
7305
  ...REQUEST_HEADERS,
@@ -9539,15 +9539,15 @@ var init_context2 = __esm({
9539
9539
  enumerable: false,
9540
9540
  configurable: true
9541
9541
  });
9542
- CoreContext2.prototype.updateEvent = function(path2, val) {
9542
+ CoreContext2.prototype.updateEvent = function(path3, val) {
9543
9543
  var _a;
9544
- if (path2.split(".")[0] === "integrations") {
9545
- var integrationName = path2.split(".")[1];
9544
+ if (path3.split(".")[0] === "integrations") {
9545
+ var integrationName = path3.split(".")[1];
9546
9546
  if (((_a = this.event.integrations) === null || _a === void 0 ? void 0 : _a[integrationName]) === false) {
9547
9547
  return this.event;
9548
9548
  }
9549
9549
  }
9550
- dset(this.event, path2, val);
9550
+ dset(this.event, path3, val);
9551
9551
  return this.event;
9552
9552
  };
9553
9553
  CoreContext2.prototype.failedDelivery = function() {
@@ -10207,8 +10207,8 @@ var stripTrailingSlash, tryCreateFormattedUrl;
10207
10207
  var init_create_url = __esm({
10208
10208
  "../../node_modules/.pnpm/@segment+analytics-node@3.1.0/node_modules/@segment/analytics-node/dist/esm/lib/create-url.js"() {
10209
10209
  stripTrailingSlash = (str) => str.replace(/\/$/, "");
10210
- tryCreateFormattedUrl = (host, path2) => {
10211
- return stripTrailingSlash(new URL(path2 || "", host).href);
10210
+ tryCreateFormattedUrl = (host, path3) => {
10211
+ return stripTrailingSlash(new URL(path3 || "", host).href);
10212
10212
  };
10213
10213
  }
10214
10214
  });
@@ -11616,12 +11616,12 @@ var init_publisher = __esm({
11616
11616
  _rateLimitedUntil;
11617
11617
  _rateLimitStartTime;
11618
11618
  _abortController = new AbortController();
11619
- constructor({ host, path: path2, maxRetries, flushAt, flushInterval, writeKey, httpRequestTimeout, httpClient, disable, oauthSettings, maxTotalBackoffDuration, maxRateLimitDuration }, emitter) {
11619
+ constructor({ host, path: path3, maxRetries, flushAt, flushInterval, writeKey, httpRequestTimeout, httpClient, disable, oauthSettings, maxTotalBackoffDuration, maxRateLimitDuration }, emitter) {
11620
11620
  this._emitter = emitter;
11621
11621
  this._maxRetries = maxRetries;
11622
11622
  this._flushAt = Math.max(flushAt, 1);
11623
11623
  this._flushInterval = flushInterval;
11624
- this._url = tryCreateFormattedUrl(host ?? "https://api.segment.io", path2 ?? "/v1/batch");
11624
+ this._url = tryCreateFormattedUrl(host ?? "https://api.segment.io", path3 ?? "/v1/batch");
11625
11625
  this._httpRequestTimeout = httpRequestTimeout ?? 1e4;
11626
11626
  this._disable = Boolean(disable);
11627
11627
  this._httpClient = httpClient;
@@ -13780,12 +13780,12 @@ var init_debug = __esm({
13780
13780
  };
13781
13781
  };
13782
13782
  };
13783
- getValueByStringPath = function(obj, path2) {
13783
+ getValueByStringPath = function(obj, path3) {
13784
13784
  var e_1, _a;
13785
- path2 = path2.replace(/\[(\w+)\]/g, ".$1");
13786
- path2 = path2.replace(/^\./, "");
13785
+ path3 = path3.replace(/\[(\w+)\]/g, ".$1");
13786
+ path3 = path3.replace(/^\./, "");
13787
13787
  try {
13788
- for (var _b = __values(path2.split(".")), _c = _b.next(); !_c.done; _c = _b.next()) {
13788
+ for (var _b = __values(path3.split(".")), _c = _b.next(); !_c.done; _c = _b.next()) {
13789
13789
  var attr = _c.value;
13790
13790
  if (attr in obj) {
13791
13791
  obj = obj[attr];
@@ -13810,8 +13810,8 @@ var init_debug = __esm({
13810
13810
  var res = {};
13811
13811
  try {
13812
13812
  for (var paths_1 = __values(paths), paths_1_1 = paths_1.next(); !paths_1_1.done; paths_1_1 = paths_1.next()) {
13813
- var path2 = paths_1_1.value;
13814
- res[path2] = getValueByStringPath(client, path2);
13813
+ var path3 = paths_1_1.value;
13814
+ res[path3] = getValueByStringPath(client, path3);
13815
13815
  }
13816
13816
  } catch (e_2_1) {
13817
13817
  e_2 = { error: e_2_1 };
@@ -16350,9 +16350,9 @@ function walk(value, prefix, out) {
16350
16350
  }
16351
16351
  if (typeof value === "object") {
16352
16352
  for (const [key, v] of Object.entries(value)) {
16353
- const path2 = prefix === "" ? key : `${prefix}.${key}`;
16354
- out.add(path2);
16355
- walk(v, path2, out);
16353
+ const path3 = prefix === "" ? key : `${prefix}.${key}`;
16354
+ out.add(path3);
16355
+ walk(v, path3, out);
16356
16356
  }
16357
16357
  return;
16358
16358
  }
@@ -16367,8 +16367,8 @@ function extractFieldStats(value, prefix = "") {
16367
16367
  const out = /* @__PURE__ */ new Map();
16368
16368
  walkFieldStats(value, prefix, out);
16369
16369
  const obj = {};
16370
- for (const [path2, stats] of out) {
16371
- obj[path2] = {
16370
+ for (const [path3, stats] of out) {
16371
+ obj[path3] = {
16372
16372
  types: [...stats.types].toSorted(),
16373
16373
  sampleValues: [...stats.sampleValues].toSorted(comparePrimitiveValues)
16374
16374
  };
@@ -16384,20 +16384,20 @@ function walkFieldStats(value, prefix, out) {
16384
16384
  }
16385
16385
  if (typeof value === "object") {
16386
16386
  for (const [key, v] of Object.entries(value)) {
16387
- const path2 = prefix === "" ? key : `${prefix}.${key}`;
16388
- addPathValue(path2, v, out);
16389
- walkFieldStats(v, path2, out);
16387
+ const path3 = prefix === "" ? key : `${prefix}.${key}`;
16388
+ addPathValue(path3, v, out);
16389
+ walkFieldStats(v, path3, out);
16390
16390
  }
16391
16391
  }
16392
16392
  }
16393
- function addPathValue(path2, value, out) {
16394
- const existing = out.get(path2) ?? { types: /* @__PURE__ */ new Set(), sampleValues: /* @__PURE__ */ new Set() };
16393
+ function addPathValue(path3, value, out) {
16394
+ const existing = out.get(path3) ?? { types: /* @__PURE__ */ new Set(), sampleValues: /* @__PURE__ */ new Set() };
16395
16395
  const t = classifyValueType(value);
16396
16396
  existing.types.add(t);
16397
16397
  if ((value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") && existing.sampleValues.size < 20) {
16398
16398
  existing.sampleValues.add(value);
16399
16399
  }
16400
- out.set(path2, existing);
16400
+ out.set(path3, existing);
16401
16401
  }
16402
16402
  function classifyValueType(value) {
16403
16403
  if (value === null) return "null";
@@ -16406,15 +16406,15 @@ function classifyValueType(value) {
16406
16406
  }
16407
16407
  function mergeFieldStats(a, b) {
16408
16408
  const merged = { ...a };
16409
- for (const [path2, bs] of Object.entries(b)) {
16410
- const prev = merged[path2];
16409
+ for (const [path3, bs] of Object.entries(b)) {
16410
+ const prev = merged[path3];
16411
16411
  if (!prev) {
16412
- merged[path2] = bs;
16412
+ merged[path3] = bs;
16413
16413
  continue;
16414
16414
  }
16415
16415
  const types4 = /* @__PURE__ */ new Set([...prev.types, ...bs.types]);
16416
16416
  const sampleValues = /* @__PURE__ */ new Set([...prev.sampleValues, ...bs.sampleValues]);
16417
- merged[path2] = {
16417
+ merged[path3] = {
16418
16418
  types: [...types4].toSorted(),
16419
16419
  sampleValues: [...sampleValues].toSorted(comparePrimitiveValues).slice(0, 20)
16420
16420
  };
@@ -16424,10 +16424,10 @@ function mergeFieldStats(a, b) {
16424
16424
  function sortFieldStats(stats) {
16425
16425
  if (!stats) return void 0;
16426
16426
  const out = {};
16427
- for (const path2 of Object.keys(stats).toSorted()) {
16428
- out[path2] = {
16429
- types: [...stats[path2].types].toSorted(),
16430
- sampleValues: [...stats[path2].sampleValues].toSorted(
16427
+ for (const path3 of Object.keys(stats).toSorted()) {
16428
+ out[path3] = {
16429
+ types: [...stats[path3].types].toSorted(),
16430
+ sampleValues: [...stats[path3].sampleValues].toSorted(
16431
16431
  comparePrimitiveValues
16432
16432
  )
16433
16433
  };
@@ -16439,6 +16439,51 @@ function comparePrimitiveValues(a, b) {
16439
16439
  const sb = JSON.stringify(b);
16440
16440
  return sa.localeCompare(sb);
16441
16441
  }
16442
+ var FileSubscriber = class extends EventSubscriber {
16443
+ name = "FileSubscriber";
16444
+ version = "1.0.0";
16445
+ filePath;
16446
+ pretty;
16447
+ ensureDir;
16448
+ transform;
16449
+ /** Serializes writes so concurrent events never interleave on disk. */
16450
+ writeChain = Promise.resolve();
16451
+ dirEnsured = false;
16452
+ constructor(config) {
16453
+ super();
16454
+ this.filePath = config.path;
16455
+ this.enabled = config.enabled ?? true;
16456
+ this.pretty = config.pretty ?? false;
16457
+ this.ensureDir = config.mkdir ?? true;
16458
+ this.transform = config.transform;
16459
+ }
16460
+ async sendToDestination(payload) {
16461
+ if (!this.enabled) return;
16462
+ const record = this.transform ? this.transform(payload) : payload;
16463
+ if (record === null) return;
16464
+ const json = this.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);
16465
+ const line = `${json}
16466
+ `;
16467
+ const run = this.writeChain.then(() => this.write(line));
16468
+ this.writeChain = run.catch(() => {
16469
+ });
16470
+ await run;
16471
+ }
16472
+ async write(line) {
16473
+ if (this.ensureDir && !this.dirEnsured) {
16474
+ const dir = path__default.default.dirname(this.filePath);
16475
+ if (dir && dir !== ".") {
16476
+ await fs.mkdir(dir, { recursive: true });
16477
+ }
16478
+ this.dirEnsured = true;
16479
+ }
16480
+ await fs.appendFile(this.filePath, line, "utf8");
16481
+ }
16482
+ async shutdown() {
16483
+ await this.writeChain;
16484
+ await super.shutdown();
16485
+ }
16486
+ };
16442
16487
 
16443
16488
  // src/streaming-event-subscriber.ts
16444
16489
  var StreamingEventSubscriber = class extends EventSubscriber {
@@ -16596,6 +16641,7 @@ exports.ARCHITECTURE_SNAPSHOT_SPEC = ARCHITECTURE_SNAPSHOT_SPEC;
16596
16641
  exports.AmplitudeSubscriber = AmplitudeSubscriber;
16597
16642
  exports.ArchitectureSnapshotSubscriber = ArchitectureSnapshotSubscriber;
16598
16643
  exports.EventSubscriber = EventSubscriber;
16644
+ exports.FileSubscriber = FileSubscriber;
16599
16645
  exports.MixpanelSubscriber = MixpanelSubscriber;
16600
16646
  exports.PostHogSubscriber = PostHogSubscriber;
16601
16647
  exports.SegmentSubscriber = SegmentSubscriber;