@metrone-io/server 1.0.1 → 1.1.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.
package/dist/index.d.cts CHANGED
@@ -4,7 +4,12 @@
4
4
  * These types mirror the Worker's RawEventPayload exactly so payloads pass
5
5
  * validation on the ingestion side without translation.
6
6
  */
7
- type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing';
7
+ /**
8
+ * Canonical source values. Mirrors the analytics_events.source CHECK
9
+ * constraint (expanded in v1.1 by migration _069).
10
+ * v1.1 SPEC-01 adds: campaign, social, review, ad, agent_action.
11
+ */
12
+ type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing' | 'campaign' | 'social' | 'review' | 'ad' | 'agent_action';
8
13
  interface MetroneServerConfig {
9
14
  /** API key (required). Format: metrone_live_* or metrone_test_* */
10
15
  apiKey: string;
@@ -52,6 +57,13 @@ interface EventPayload {
52
57
  agent_type?: string;
53
58
  /** Idempotency key to prevent duplicate processing */
54
59
  idempotency_key?: string;
60
+ /**
61
+ * Original visitor User-Agent, for middleware/edge senders forwarding
62
+ * traffic they observed. The worker parses it for device and visitor
63
+ * classification (human vs ai_agent vs bot) and then discards it — the
64
+ * raw UA is never stored. Without it, the ingest request's own UA is used.
65
+ */
66
+ user_agent?: string;
55
67
  properties?: Record<string, unknown>;
56
68
  timestamp?: string;
57
69
  }
@@ -271,6 +283,67 @@ declare class MetroneServer {
271
283
  * Track an AI session lifecycle event (start, end, timeout).
272
284
  */
273
285
  trackAISession(data: AISessionData): void;
286
+ /**
287
+ * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
288
+ * Recommended event_type values: email_sent | email_delivered |
289
+ * email_opened | email_clicked | email_bounced | email_unsubscribed |
290
+ * sms_sent | sms_delivered | sms_replied | sms_opt_out
291
+ */
292
+ trackCampaignEvent(data: {
293
+ event_type: string;
294
+ campaign_id: string;
295
+ recipient_id_hash: string;
296
+ } & Record<string, unknown>): void;
297
+ /**
298
+ * Track a social media event. Recommended event_type values:
299
+ * post_published | impression | engagement | click | share
300
+ */
301
+ trackSocialEvent(data: {
302
+ event_type: string;
303
+ platform: string;
304
+ post_id: string;
305
+ } & Record<string, unknown>): void;
306
+ /**
307
+ * Track a review event. Recommended event_type values: received | responded
308
+ */
309
+ trackReviewEvent(data: {
310
+ event_type: string;
311
+ platform: string;
312
+ review_id: string;
313
+ rating: number;
314
+ } & Record<string, unknown>): void;
315
+ /**
316
+ * Track a paid-ad metrics event. Recommended event_type values:
317
+ * impression | click | spend_daily | conversion
318
+ */
319
+ trackAdEvent(data: {
320
+ event_type: string;
321
+ platform: string;
322
+ campaign_id: string;
323
+ } & Record<string, unknown>): void;
324
+ /**
325
+ * Track an SMS conversation event. Recommended event_type values:
326
+ * received | replied. agent_id is promoted to the top-level column so
327
+ * existing per-agent dashboard breakdowns keep working.
328
+ */
329
+ trackSmsEvent(data: {
330
+ event_type: string;
331
+ agent_id: string;
332
+ } & Record<string, unknown>): void;
333
+ /**
334
+ * Track an autonomous agent action (audit-log style). event_type is the
335
+ * action itself (free-form vocabulary). agent_type / agent_id are written
336
+ * BOTH to the top-level columns (for existing dashboard breakdowns) AND
337
+ * into properties (for the JSONB-first get_agent_activity_feed RPC) —
338
+ * decision B4 of the v1.1 build.
339
+ */
340
+ trackAgentAction(data: {
341
+ agent_type: string;
342
+ action_taken: string;
343
+ agent_id?: string;
344
+ target?: string;
345
+ outcome?: string;
346
+ } & Record<string, unknown>): void;
274
347
  /**
275
348
  * Get aggregated analytics stats for a time period.
276
349
  */
@@ -309,6 +382,98 @@ declare class MetroneServer {
309
382
  private buildDateParams;
310
383
  }
311
384
 
385
+ /**
386
+ * agent-tracker.ts — server-side capture of AI agent traffic.
387
+ *
388
+ * AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages
389
+ * without executing JavaScript, so the browser tracker never sees them. This
390
+ * module runs where the request is actually served — an Express/Connect
391
+ * middleware, a fetch-style handler wrapper, or a manual call — and forwards
392
+ * matched requests to Metrone with the original User-Agent in the
393
+ * `user_agent` payload field. The ingestion worker performs the
394
+ * authoritative classification (visitor_type = 'ai_agent') and the raw UA
395
+ * is never stored.
396
+ *
397
+ * Only requests whose UA matches the AI-agent list are forwarded — normal
398
+ * human traffic is never sent from here (the browser tracker handles it),
399
+ * so this adds zero overhead and zero quota usage for regular visitors.
400
+ *
401
+ * Keep the UA list in sync with AI_AGENT_RULES in
402
+ * packages/worker/src/lib/device-parser.ts (the authoritative matcher).
403
+ */
404
+
405
+ /**
406
+ * Returns the canonical AI agent name for a User-Agent, or undefined when
407
+ * the UA does not belong to a known AI agent.
408
+ */
409
+ declare function matchAiAgent(userAgent: string | null | undefined): string | undefined;
410
+ interface AgentHit {
411
+ /** Full request URL (or at minimum a path). */
412
+ url: string;
413
+ /** The visitor's User-Agent header. */
414
+ userAgent: string | null | undefined;
415
+ /** Optional Referer header. */
416
+ referrer?: string | null;
417
+ /** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */
418
+ method?: string;
419
+ }
420
+ interface AgentTrackerOptions {
421
+ /**
422
+ * Skip requests for static assets (css/js/images/fonts/…). Default: true.
423
+ */
424
+ skipStaticAssets?: boolean;
425
+ /**
426
+ * Extra paths to ignore (exact match or prefix ending with '*'),
427
+ * e.g. ['/health', '/api/*'].
428
+ */
429
+ ignorePaths?: string[];
430
+ }
431
+ /**
432
+ * Core capture: if the request comes from a known AI agent, forward it to
433
+ * Metrone as a pageview with the original UA. Fire-and-forget — never
434
+ * throws, never blocks the response.
435
+ *
436
+ * Returns the matched agent name, or undefined when the request was not an
437
+ * AI agent (or was filtered out).
438
+ *
439
+ * Serverless note: the SDK batches by default; in short-lived environments
440
+ * construct the client with `batchSize: 0` so hits are sent immediately.
441
+ */
442
+ declare function captureAgentHit(metrone: MetroneServer, hit: AgentHit, options?: AgentTrackerOptions): string | undefined;
443
+ /**
444
+ * Express / Connect middleware. Mount early, before routing:
445
+ *
446
+ * ```ts
447
+ * import { MetroneServer, agentMiddleware } from '@metrone-io/server'
448
+ *
449
+ * const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })
450
+ * app.use(agentMiddleware(metrone))
451
+ * ```
452
+ */
453
+ declare function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions): (req: {
454
+ method?: string;
455
+ originalUrl?: string;
456
+ url?: string;
457
+ headers: Record<string, string | string[] | undefined>;
458
+ protocol?: string;
459
+ get?: (name: string) => string | undefined;
460
+ }, _res: unknown, next: (err?: unknown) => void) => void;
461
+ /**
462
+ * WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,
463
+ * Deno, and Cloudflare Workers:
464
+ *
465
+ * ```ts
466
+ * trackAgentRequest(metrone, request)
467
+ * ```
468
+ */
469
+ declare function trackAgentRequest(metrone: MetroneServer, request: {
470
+ url: string;
471
+ method?: string;
472
+ headers: {
473
+ get(name: string): string | null;
474
+ };
475
+ }, options?: AgentTrackerOptions): string | undefined;
476
+
312
477
  /**
313
478
  * Structured error types for the Metrone server SDK.
314
479
  *
@@ -354,4 +519,4 @@ declare class MetroneServerError extends MetroneError {
354
519
  constructor(status: number, message?: string);
355
520
  }
356
521
 
357
- export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse };
522
+ export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type AgentHit, type AgentTrackerOptions, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse, agentMiddleware, captureAgentHit, matchAiAgent, trackAgentRequest };
package/dist/index.d.ts CHANGED
@@ -4,7 +4,12 @@
4
4
  * These types mirror the Worker's RawEventPayload exactly so payloads pass
5
5
  * validation on the ingestion side without translation.
6
6
  */
7
- type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing';
7
+ /**
8
+ * Canonical source values. Mirrors the analytics_events.source CHECK
9
+ * constraint (expanded in v1.1 by migration _069).
10
+ * v1.1 SPEC-01 adds: campaign, social, review, ad, agent_action.
11
+ */
12
+ type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing' | 'campaign' | 'social' | 'review' | 'ad' | 'agent_action';
8
13
  interface MetroneServerConfig {
9
14
  /** API key (required). Format: metrone_live_* or metrone_test_* */
10
15
  apiKey: string;
@@ -52,6 +57,13 @@ interface EventPayload {
52
57
  agent_type?: string;
53
58
  /** Idempotency key to prevent duplicate processing */
54
59
  idempotency_key?: string;
60
+ /**
61
+ * Original visitor User-Agent, for middleware/edge senders forwarding
62
+ * traffic they observed. The worker parses it for device and visitor
63
+ * classification (human vs ai_agent vs bot) and then discards it — the
64
+ * raw UA is never stored. Without it, the ingest request's own UA is used.
65
+ */
66
+ user_agent?: string;
55
67
  properties?: Record<string, unknown>;
56
68
  timestamp?: string;
57
69
  }
@@ -271,6 +283,67 @@ declare class MetroneServer {
271
283
  * Track an AI session lifecycle event (start, end, timeout).
272
284
  */
273
285
  trackAISession(data: AISessionData): void;
286
+ /**
287
+ * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
288
+ * Recommended event_type values: email_sent | email_delivered |
289
+ * email_opened | email_clicked | email_bounced | email_unsubscribed |
290
+ * sms_sent | sms_delivered | sms_replied | sms_opt_out
291
+ */
292
+ trackCampaignEvent(data: {
293
+ event_type: string;
294
+ campaign_id: string;
295
+ recipient_id_hash: string;
296
+ } & Record<string, unknown>): void;
297
+ /**
298
+ * Track a social media event. Recommended event_type values:
299
+ * post_published | impression | engagement | click | share
300
+ */
301
+ trackSocialEvent(data: {
302
+ event_type: string;
303
+ platform: string;
304
+ post_id: string;
305
+ } & Record<string, unknown>): void;
306
+ /**
307
+ * Track a review event. Recommended event_type values: received | responded
308
+ */
309
+ trackReviewEvent(data: {
310
+ event_type: string;
311
+ platform: string;
312
+ review_id: string;
313
+ rating: number;
314
+ } & Record<string, unknown>): void;
315
+ /**
316
+ * Track a paid-ad metrics event. Recommended event_type values:
317
+ * impression | click | spend_daily | conversion
318
+ */
319
+ trackAdEvent(data: {
320
+ event_type: string;
321
+ platform: string;
322
+ campaign_id: string;
323
+ } & Record<string, unknown>): void;
324
+ /**
325
+ * Track an SMS conversation event. Recommended event_type values:
326
+ * received | replied. agent_id is promoted to the top-level column so
327
+ * existing per-agent dashboard breakdowns keep working.
328
+ */
329
+ trackSmsEvent(data: {
330
+ event_type: string;
331
+ agent_id: string;
332
+ } & Record<string, unknown>): void;
333
+ /**
334
+ * Track an autonomous agent action (audit-log style). event_type is the
335
+ * action itself (free-form vocabulary). agent_type / agent_id are written
336
+ * BOTH to the top-level columns (for existing dashboard breakdowns) AND
337
+ * into properties (for the JSONB-first get_agent_activity_feed RPC) —
338
+ * decision B4 of the v1.1 build.
339
+ */
340
+ trackAgentAction(data: {
341
+ agent_type: string;
342
+ action_taken: string;
343
+ agent_id?: string;
344
+ target?: string;
345
+ outcome?: string;
346
+ } & Record<string, unknown>): void;
274
347
  /**
275
348
  * Get aggregated analytics stats for a time period.
276
349
  */
@@ -309,6 +382,98 @@ declare class MetroneServer {
309
382
  private buildDateParams;
310
383
  }
311
384
 
385
+ /**
386
+ * agent-tracker.ts — server-side capture of AI agent traffic.
387
+ *
388
+ * AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages
389
+ * without executing JavaScript, so the browser tracker never sees them. This
390
+ * module runs where the request is actually served — an Express/Connect
391
+ * middleware, a fetch-style handler wrapper, or a manual call — and forwards
392
+ * matched requests to Metrone with the original User-Agent in the
393
+ * `user_agent` payload field. The ingestion worker performs the
394
+ * authoritative classification (visitor_type = 'ai_agent') and the raw UA
395
+ * is never stored.
396
+ *
397
+ * Only requests whose UA matches the AI-agent list are forwarded — normal
398
+ * human traffic is never sent from here (the browser tracker handles it),
399
+ * so this adds zero overhead and zero quota usage for regular visitors.
400
+ *
401
+ * Keep the UA list in sync with AI_AGENT_RULES in
402
+ * packages/worker/src/lib/device-parser.ts (the authoritative matcher).
403
+ */
404
+
405
+ /**
406
+ * Returns the canonical AI agent name for a User-Agent, or undefined when
407
+ * the UA does not belong to a known AI agent.
408
+ */
409
+ declare function matchAiAgent(userAgent: string | null | undefined): string | undefined;
410
+ interface AgentHit {
411
+ /** Full request URL (or at minimum a path). */
412
+ url: string;
413
+ /** The visitor's User-Agent header. */
414
+ userAgent: string | null | undefined;
415
+ /** Optional Referer header. */
416
+ referrer?: string | null;
417
+ /** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */
418
+ method?: string;
419
+ }
420
+ interface AgentTrackerOptions {
421
+ /**
422
+ * Skip requests for static assets (css/js/images/fonts/…). Default: true.
423
+ */
424
+ skipStaticAssets?: boolean;
425
+ /**
426
+ * Extra paths to ignore (exact match or prefix ending with '*'),
427
+ * e.g. ['/health', '/api/*'].
428
+ */
429
+ ignorePaths?: string[];
430
+ }
431
+ /**
432
+ * Core capture: if the request comes from a known AI agent, forward it to
433
+ * Metrone as a pageview with the original UA. Fire-and-forget — never
434
+ * throws, never blocks the response.
435
+ *
436
+ * Returns the matched agent name, or undefined when the request was not an
437
+ * AI agent (or was filtered out).
438
+ *
439
+ * Serverless note: the SDK batches by default; in short-lived environments
440
+ * construct the client with `batchSize: 0` so hits are sent immediately.
441
+ */
442
+ declare function captureAgentHit(metrone: MetroneServer, hit: AgentHit, options?: AgentTrackerOptions): string | undefined;
443
+ /**
444
+ * Express / Connect middleware. Mount early, before routing:
445
+ *
446
+ * ```ts
447
+ * import { MetroneServer, agentMiddleware } from '@metrone-io/server'
448
+ *
449
+ * const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })
450
+ * app.use(agentMiddleware(metrone))
451
+ * ```
452
+ */
453
+ declare function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions): (req: {
454
+ method?: string;
455
+ originalUrl?: string;
456
+ url?: string;
457
+ headers: Record<string, string | string[] | undefined>;
458
+ protocol?: string;
459
+ get?: (name: string) => string | undefined;
460
+ }, _res: unknown, next: (err?: unknown) => void) => void;
461
+ /**
462
+ * WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,
463
+ * Deno, and Cloudflare Workers:
464
+ *
465
+ * ```ts
466
+ * trackAgentRequest(metrone, request)
467
+ * ```
468
+ */
469
+ declare function trackAgentRequest(metrone: MetroneServer, request: {
470
+ url: string;
471
+ method?: string;
472
+ headers: {
473
+ get(name: string): string | null;
474
+ };
475
+ }, options?: AgentTrackerOptions): string | undefined;
476
+
312
477
  /**
313
478
  * Structured error types for the Metrone server SDK.
314
479
  *
@@ -354,4 +519,4 @@ declare class MetroneServerError extends MetroneError {
354
519
  constructor(status: number, message?: string);
355
520
  }
356
521
 
357
- export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse };
522
+ export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type AgentHit, type AgentTrackerOptions, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse, agentMiddleware, captureAgentHit, matchAiAgent, trackAgentRequest };
package/dist/index.js CHANGED
@@ -64,6 +64,12 @@ var MetroneServerError = class extends MetroneError {
64
64
  this.name = "MetroneServerError";
65
65
  }
66
66
  };
67
+ var MetroneClientError = class extends MetroneError {
68
+ constructor(status, message = "Request rejected by server") {
69
+ super("CLIENT_ERROR", message, status, false);
70
+ this.name = "MetroneClientError";
71
+ }
72
+ };
67
73
 
68
74
  // src/http.ts
69
75
  async function httpPost(config, path, body, headers) {
@@ -86,7 +92,7 @@ async function httpRequest(config, method, path, body, extraHeaders) {
86
92
  const fetchFn = config.fetch;
87
93
  const headers = {
88
94
  "X-Api-Key": config.apiKey,
89
- "User-Agent": "metrone-server-sdk/1.0.0",
95
+ "User-Agent": "metrone-server-sdk/1.0.2",
90
96
  ...extraHeaders
91
97
  };
92
98
  if (body !== void 0) {
@@ -120,6 +126,9 @@ async function httpRequest(config, method, path, body, extraHeaders) {
120
126
  throw new MetroneRateLimitError(retryMs);
121
127
  }
122
128
  if (response.status >= 500) throw new MetroneServerError(response.status, errMsg);
129
+ if (response.status >= 400 && response.status < 500) {
130
+ throw new MetroneClientError(response.status, errMsg);
131
+ }
123
132
  return {
124
133
  ok: false,
125
134
  status: response.status,
@@ -130,6 +139,7 @@ async function httpRequest(config, method, path, body, extraHeaders) {
130
139
  if (err instanceof MetroneAuthError) throw err;
131
140
  if (err instanceof MetroneRateLimitError) throw err;
132
141
  if (err instanceof MetroneServerError) throw err;
142
+ if (err instanceof MetroneClientError) throw err;
133
143
  if (err instanceof DOMException && err.name === "AbortError") {
134
144
  throw new MetroneTimeoutError(config.timeoutMs);
135
145
  }
@@ -328,6 +338,68 @@ var MetroneServer = class {
328
338
  properties: data.properties
329
339
  });
330
340
  }
341
+ // ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────
342
+ // Thin ergonomic wrappers over track(). Required fields are typed; any
343
+ // extra keys flow into the JSONB `properties` payload (storage acceptance
344
+ // is permissive — see @metrone/schemas per-source recommended shapes).
345
+ /**
346
+ * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
347
+ * Recommended event_type values: email_sent | email_delivered |
348
+ * email_opened | email_clicked | email_bounced | email_unsubscribed |
349
+ * sms_sent | sms_delivered | sms_replied | sms_opt_out
350
+ */
351
+ trackCampaignEvent(data) {
352
+ const { event_type, ...properties } = data;
353
+ this.track(event_type, { source: "campaign", properties });
354
+ }
355
+ /**
356
+ * Track a social media event. Recommended event_type values:
357
+ * post_published | impression | engagement | click | share
358
+ */
359
+ trackSocialEvent(data) {
360
+ const { event_type, ...properties } = data;
361
+ this.track(event_type, { source: "social", properties });
362
+ }
363
+ /**
364
+ * Track a review event. Recommended event_type values: received | responded
365
+ */
366
+ trackReviewEvent(data) {
367
+ const { event_type, ...properties } = data;
368
+ this.track(event_type, { source: "review", properties });
369
+ }
370
+ /**
371
+ * Track a paid-ad metrics event. Recommended event_type values:
372
+ * impression | click | spend_daily | conversion
373
+ */
374
+ trackAdEvent(data) {
375
+ const { event_type, ...properties } = data;
376
+ this.track(event_type, { source: "ad", properties });
377
+ }
378
+ /**
379
+ * Track an SMS conversation event. Recommended event_type values:
380
+ * received | replied. agent_id is promoted to the top-level column so
381
+ * existing per-agent dashboard breakdowns keep working.
382
+ */
383
+ trackSmsEvent(data) {
384
+ const { event_type, agent_id, ...properties } = data;
385
+ this.track(event_type, { source: "sms", agent_id, properties });
386
+ }
387
+ /**
388
+ * Track an autonomous agent action (audit-log style). event_type is the
389
+ * action itself (free-form vocabulary). agent_type / agent_id are written
390
+ * BOTH to the top-level columns (for existing dashboard breakdowns) AND
391
+ * into properties (for the JSONB-first get_agent_activity_feed RPC) —
392
+ * decision B4 of the v1.1 build.
393
+ */
394
+ trackAgentAction(data) {
395
+ const { agent_type, action_taken, agent_id, ...rest } = data;
396
+ this.track(action_taken, {
397
+ source: "agent_action",
398
+ agent_id,
399
+ agent_type,
400
+ properties: { agent_type, action_taken, ...rest }
401
+ });
402
+ }
331
403
  // ─── Read API ──────────────────────────────────────────────────────────────
332
404
  /**
333
405
  * Get aggregated analytics stats for a time period.
@@ -420,12 +492,26 @@ var MetroneServer = class {
420
492
  }
421
493
  return { sent: accepted, failed: batch.length - accepted, errors: [] };
422
494
  } catch (err) {
495
+ const isPermanent = err instanceof MetroneAuthError || err instanceof MetroneClientError || err instanceof MetroneError && err.retryable === false;
496
+ if (isPermanent) {
497
+ if (this.config.debug) {
498
+ console.error(
499
+ `[metrone] Dropping ${batch.length} events \u2014 server rejected them permanently:`,
500
+ err instanceof Error ? err.message : err
501
+ );
502
+ }
503
+ return {
504
+ sent: 0,
505
+ failed: batch.length,
506
+ errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }]
507
+ };
508
+ }
423
509
  this.queue.unshift(...batch);
424
510
  if (this.queue.length > this.config.maxQueueSize) {
425
511
  this.queue.length = this.config.maxQueueSize;
426
512
  }
427
513
  if (this.config.debug) {
428
- console.error("[metrone] Flush failed, re-queued:", err);
514
+ console.error("[metrone] Flush failed (transient), re-queued:", err);
429
515
  }
430
516
  return {
431
517
  sent: 0,
@@ -479,6 +565,119 @@ var MetroneServer = class {
479
565
  return result;
480
566
  }
481
567
  };
568
+
569
+ // src/agent-tracker.ts
570
+ var AI_AGENT_RULES = [
571
+ // OpenAI
572
+ { pattern: /chatgpt-user/i, name: "ChatGPT-User" },
573
+ { pattern: /oai-searchbot/i, name: "OAI-SearchBot" },
574
+ { pattern: /gptbot/i, name: "GPTBot" },
575
+ // Anthropic
576
+ { pattern: /claude-user/i, name: "Claude-User" },
577
+ { pattern: /claude-searchbot/i, name: "Claude-SearchBot" },
578
+ { pattern: /claudebot/i, name: "ClaudeBot" },
579
+ { pattern: /claude-web/i, name: "Claude-Web" },
580
+ { pattern: /anthropic-ai/i, name: "anthropic-ai" },
581
+ // Perplexity
582
+ { pattern: /perplexity-user/i, name: "Perplexity-User" },
583
+ { pattern: /perplexitybot/i, name: "PerplexityBot" },
584
+ // Google AI
585
+ { pattern: /google-cloudvertexbot/i, name: "Google-CloudVertexBot" },
586
+ { pattern: /googleother/i, name: "GoogleOther" },
587
+ { pattern: /gemini-deep-research/i, name: "Gemini-Deep-Research" },
588
+ // Meta
589
+ { pattern: /meta-externalagent/i, name: "Meta-ExternalAgent" },
590
+ { pattern: /meta-externalfetcher/i, name: "Meta-ExternalFetcher" },
591
+ // Microsoft Copilot
592
+ { pattern: /bingpreview/i, name: "BingPreview" },
593
+ // xAI
594
+ { pattern: /grokbot/i, name: "GrokBot" },
595
+ { pattern: /xai-crawler/i, name: "xAI-Crawler" },
596
+ // Others
597
+ { pattern: /duckassistbot/i, name: "DuckAssistBot" },
598
+ { pattern: /mistralai-user/i, name: "MistralAI-User" },
599
+ { pattern: /cohere-training-data-crawler/i, name: "Cohere-Training-Crawler" },
600
+ { pattern: /cohere-ai/i, name: "cohere-ai" },
601
+ { pattern: /bytespider/i, name: "Bytespider" },
602
+ { pattern: /amazonbot/i, name: "Amazonbot" },
603
+ { pattern: /applebot-extended/i, name: "Applebot-Extended" },
604
+ { pattern: /ccbot/i, name: "CCBot" },
605
+ { pattern: /youbot/i, name: "YouBot" },
606
+ { pattern: /timpibot/i, name: "TimpiBot" },
607
+ { pattern: /diffbot/i, name: "Diffbot" }
608
+ ];
609
+ var STATIC_ASSET_PATTERN = /\.(css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|otf|eot|map|pdf|zip|gz|mp4|webm|mp3)$/i;
610
+ function matchAiAgent(userAgent) {
611
+ if (!userAgent) return void 0;
612
+ for (const rule of AI_AGENT_RULES) {
613
+ if (rule.pattern.test(userAgent)) return rule.name;
614
+ }
615
+ return void 0;
616
+ }
617
+ function pathFromUrl(url) {
618
+ try {
619
+ return new URL(url, "http://localhost").pathname;
620
+ } catch {
621
+ return url.split("?")[0] ?? url;
622
+ }
623
+ }
624
+ function isIgnored(path, options) {
625
+ if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true;
626
+ for (const rule of options?.ignorePaths ?? []) {
627
+ if (rule.endsWith("*") ? path.startsWith(rule.slice(0, -1)) : path === rule) return true;
628
+ }
629
+ return false;
630
+ }
631
+ function captureAgentHit(metrone, hit, options) {
632
+ try {
633
+ const method = (hit.method ?? "GET").toUpperCase();
634
+ if (method !== "GET" && method !== "HEAD") return void 0;
635
+ const agent = matchAiAgent(hit.userAgent);
636
+ if (!agent) return void 0;
637
+ const path = pathFromUrl(hit.url);
638
+ if (isIgnored(path, options)) return void 0;
639
+ metrone.track("pageview", {
640
+ source: "web",
641
+ page_url: hit.url,
642
+ page_path: path,
643
+ referrer: hit.referrer ?? void 0,
644
+ user_agent: hit.userAgent ?? void 0,
645
+ properties: { agent_capture: "server" }
646
+ });
647
+ return agent;
648
+ } catch {
649
+ return void 0;
650
+ }
651
+ }
652
+ function agentMiddleware(metrone, options) {
653
+ return function metroneAgentMiddleware(req, _res, next) {
654
+ try {
655
+ const header = (name) => {
656
+ const v = req.headers[name];
657
+ return Array.isArray(v) ? v[0] : v;
658
+ };
659
+ const host = header("host") ?? "localhost";
660
+ const proto = req.protocol ?? (header("x-forwarded-proto") ?? "https");
661
+ const rawPath = req.originalUrl ?? req.url ?? "/";
662
+ captureAgentHit(metrone, {
663
+ url: `${proto}://${host}${rawPath}`,
664
+ userAgent: header("user-agent"),
665
+ referrer: header("referer") ?? header("referrer"),
666
+ method: req.method
667
+ }, options);
668
+ } catch {
669
+ }
670
+ next();
671
+ };
672
+ }
673
+ function trackAgentRequest(metrone, request, options) {
674
+ return captureAgentHit(metrone, {
675
+ url: request.url,
676
+ userAgent: request.headers.get("user-agent"),
677
+ referrer: request.headers.get("referer"),
678
+ method: request.method
679
+ }, options);
680
+ }
482
681
  export {
483
682
  MetroneAuthError,
484
683
  MetroneConfigError,
@@ -489,6 +688,10 @@ export {
489
688
  MetroneServer,
490
689
  MetroneServerError,
491
690
  MetroneTimeoutError,
492
- MetroneValidationError
691
+ MetroneValidationError,
692
+ agentMiddleware,
693
+ captureAgentHit,
694
+ matchAiAgent,
695
+ trackAgentRequest
493
696
  };
494
697
  //# sourceMappingURL=index.js.map