@metrone-io/server 1.0.2 → 1.1.1

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
@@ -1,3 +1,23 @@
1
+ // src/url-redact.ts
2
+ function sanitizePublicUrl(url) {
3
+ if (!url) return url;
4
+ if (url.startsWith("mailto:")) {
5
+ const rest = url.slice(7).split("?")[0].split("#")[0];
6
+ const at = rest.lastIndexOf("@");
7
+ return at >= 0 ? `mailto:@${rest.slice(at + 1)}` : "mailto:";
8
+ }
9
+ const q = url.indexOf("?");
10
+ const h = url.indexOf("#");
11
+ let cut = url.length;
12
+ if (q >= 0) cut = q;
13
+ if (h >= 0 && h < cut) cut = h;
14
+ return url.slice(0, cut);
15
+ }
16
+ function sanitizePagePath(path) {
17
+ if (!path) return path;
18
+ return path.split("?")[0].split("#")[0];
19
+ }
20
+
1
21
  // src/errors.ts
2
22
  var MetroneError = class extends Error {
3
23
  code;
@@ -235,7 +255,7 @@ var MetroneServer = class {
235
255
  api_key: this.config.apiKey,
236
256
  event_type: eventType,
237
257
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
238
- ...data
258
+ ...sanitizeEventUrls(data)
239
259
  };
240
260
  if (this.config.batchSize <= 0) {
241
261
  withRetry(this.config, () => httpPost(this.config, "/v1/events", event)).catch((err) => {
@@ -338,6 +358,68 @@ var MetroneServer = class {
338
358
  properties: data.properties
339
359
  });
340
360
  }
361
+ // ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────
362
+ // Thin ergonomic wrappers over track(). Required fields are typed; any
363
+ // extra keys flow into the JSONB `properties` payload (storage acceptance
364
+ // is permissive — see @metrone/schemas per-source recommended shapes).
365
+ /**
366
+ * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
367
+ * Recommended event_type values: email_sent | email_delivered |
368
+ * email_opened | email_clicked | email_bounced | email_unsubscribed |
369
+ * sms_sent | sms_delivered | sms_replied | sms_opt_out
370
+ */
371
+ trackCampaignEvent(data) {
372
+ const { event_type, ...properties } = data;
373
+ this.track(event_type, { source: "campaign", properties });
374
+ }
375
+ /**
376
+ * Track a social media event. Recommended event_type values:
377
+ * post_published | impression | engagement | click | share
378
+ */
379
+ trackSocialEvent(data) {
380
+ const { event_type, ...properties } = data;
381
+ this.track(event_type, { source: "social", properties });
382
+ }
383
+ /**
384
+ * Track a review event. Recommended event_type values: received | responded
385
+ */
386
+ trackReviewEvent(data) {
387
+ const { event_type, ...properties } = data;
388
+ this.track(event_type, { source: "review", properties });
389
+ }
390
+ /**
391
+ * Track a paid-ad metrics event. Recommended event_type values:
392
+ * impression | click | spend_daily | conversion
393
+ */
394
+ trackAdEvent(data) {
395
+ const { event_type, ...properties } = data;
396
+ this.track(event_type, { source: "ad", properties });
397
+ }
398
+ /**
399
+ * Track an SMS conversation event. Recommended event_type values:
400
+ * received | replied. agent_id is promoted to the top-level column so
401
+ * existing per-agent dashboard breakdowns keep working.
402
+ */
403
+ trackSmsEvent(data) {
404
+ const { event_type, agent_id, ...properties } = data;
405
+ this.track(event_type, { source: "sms", agent_id, properties });
406
+ }
407
+ /**
408
+ * Track an autonomous agent action (audit-log style). event_type is the
409
+ * action itself (free-form vocabulary). agent_type / agent_id are written
410
+ * BOTH to the top-level columns (for existing dashboard breakdowns) AND
411
+ * into properties (for the JSONB-first get_agent_activity_feed RPC) —
412
+ * decision B4 of the v1.1 build.
413
+ */
414
+ trackAgentAction(data) {
415
+ const { agent_type, action_taken, agent_id, ...rest } = data;
416
+ this.track(action_taken, {
417
+ source: "agent_action",
418
+ agent_id,
419
+ agent_type,
420
+ properties: { agent_type, action_taken, ...rest }
421
+ });
422
+ }
341
423
  // ─── Read API ──────────────────────────────────────────────────────────────
342
424
  /**
343
425
  * Get aggregated analytics stats for a time period.
@@ -503,6 +585,130 @@ var MetroneServer = class {
503
585
  return result;
504
586
  }
505
587
  };
588
+ function sanitizeEventUrls(data) {
589
+ if (!data) return {};
590
+ const next = { ...data };
591
+ if (typeof next.page_url === "string") next.page_url = sanitizePublicUrl(next.page_url);
592
+ if (typeof next.page_path === "string") next.page_path = sanitizePagePath(next.page_path);
593
+ if (typeof next.referrer === "string") next.referrer = sanitizePublicUrl(next.referrer);
594
+ if (next.properties && typeof next.properties === "object" && typeof next.properties.url === "string") {
595
+ next.properties = { ...next.properties, url: sanitizePublicUrl(next.properties.url) };
596
+ }
597
+ return next;
598
+ }
599
+
600
+ // src/agent-tracker.ts
601
+ var AI_AGENT_RULES = [
602
+ // OpenAI
603
+ { pattern: /chatgpt-user/i, name: "ChatGPT-User" },
604
+ { pattern: /oai-searchbot/i, name: "OAI-SearchBot" },
605
+ { pattern: /gptbot/i, name: "GPTBot" },
606
+ // Anthropic
607
+ { pattern: /claude-user/i, name: "Claude-User" },
608
+ { pattern: /claude-searchbot/i, name: "Claude-SearchBot" },
609
+ { pattern: /claudebot/i, name: "ClaudeBot" },
610
+ { pattern: /claude-web/i, name: "Claude-Web" },
611
+ { pattern: /anthropic-ai/i, name: "anthropic-ai" },
612
+ // Perplexity
613
+ { pattern: /perplexity-user/i, name: "Perplexity-User" },
614
+ { pattern: /perplexitybot/i, name: "PerplexityBot" },
615
+ // Google AI
616
+ { pattern: /google-cloudvertexbot/i, name: "Google-CloudVertexBot" },
617
+ { pattern: /googleother/i, name: "GoogleOther" },
618
+ { pattern: /gemini-deep-research/i, name: "Gemini-Deep-Research" },
619
+ // Meta
620
+ { pattern: /meta-externalagent/i, name: "Meta-ExternalAgent" },
621
+ { pattern: /meta-externalfetcher/i, name: "Meta-ExternalFetcher" },
622
+ // Microsoft Copilot
623
+ { pattern: /bingpreview/i, name: "BingPreview" },
624
+ // xAI
625
+ { pattern: /grokbot/i, name: "GrokBot" },
626
+ { pattern: /xai-crawler/i, name: "xAI-Crawler" },
627
+ // Others
628
+ { pattern: /duckassistbot/i, name: "DuckAssistBot" },
629
+ { pattern: /mistralai-user/i, name: "MistralAI-User" },
630
+ { pattern: /cohere-training-data-crawler/i, name: "Cohere-Training-Crawler" },
631
+ { pattern: /cohere-ai/i, name: "cohere-ai" },
632
+ { pattern: /bytespider/i, name: "Bytespider" },
633
+ { pattern: /amazonbot/i, name: "Amazonbot" },
634
+ { pattern: /applebot-extended/i, name: "Applebot-Extended" },
635
+ { pattern: /ccbot/i, name: "CCBot" },
636
+ { pattern: /youbot/i, name: "YouBot" },
637
+ { pattern: /timpibot/i, name: "TimpiBot" },
638
+ { pattern: /diffbot/i, name: "Diffbot" }
639
+ ];
640
+ 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;
641
+ function matchAiAgent(userAgent) {
642
+ if (!userAgent) return void 0;
643
+ for (const rule of AI_AGENT_RULES) {
644
+ if (rule.pattern.test(userAgent)) return rule.name;
645
+ }
646
+ return void 0;
647
+ }
648
+ function pathFromUrl(url) {
649
+ try {
650
+ return new URL(url, "http://localhost").pathname;
651
+ } catch {
652
+ return url.split("?")[0] ?? url;
653
+ }
654
+ }
655
+ function isIgnored(path, options) {
656
+ if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true;
657
+ for (const rule of options?.ignorePaths ?? []) {
658
+ if (rule.endsWith("*") ? path.startsWith(rule.slice(0, -1)) : path === rule) return true;
659
+ }
660
+ return false;
661
+ }
662
+ function captureAgentHit(metrone, hit, options) {
663
+ try {
664
+ const method = (hit.method ?? "GET").toUpperCase();
665
+ if (method !== "GET" && method !== "HEAD") return void 0;
666
+ const agent = matchAiAgent(hit.userAgent);
667
+ if (!agent) return void 0;
668
+ const path = pathFromUrl(hit.url);
669
+ if (isIgnored(path, options)) return void 0;
670
+ metrone.track("pageview", {
671
+ source: "web",
672
+ page_url: hit.url,
673
+ page_path: path,
674
+ referrer: hit.referrer ?? void 0,
675
+ user_agent: hit.userAgent ?? void 0,
676
+ properties: { agent_capture: "server" }
677
+ });
678
+ return agent;
679
+ } catch {
680
+ return void 0;
681
+ }
682
+ }
683
+ function agentMiddleware(metrone, options) {
684
+ return function metroneAgentMiddleware(req, _res, next) {
685
+ try {
686
+ const header = (name) => {
687
+ const v = req.headers[name];
688
+ return Array.isArray(v) ? v[0] : v;
689
+ };
690
+ const host = header("host") ?? "localhost";
691
+ const proto = req.protocol ?? (header("x-forwarded-proto") ?? "https");
692
+ const rawPath = req.originalUrl ?? req.url ?? "/";
693
+ captureAgentHit(metrone, {
694
+ url: `${proto}://${host}${rawPath}`,
695
+ userAgent: header("user-agent"),
696
+ referrer: header("referer") ?? header("referrer"),
697
+ method: req.method
698
+ }, options);
699
+ } catch {
700
+ }
701
+ next();
702
+ };
703
+ }
704
+ function trackAgentRequest(metrone, request, options) {
705
+ return captureAgentHit(metrone, {
706
+ url: request.url,
707
+ userAgent: request.headers.get("user-agent"),
708
+ referrer: request.headers.get("referer"),
709
+ method: request.method
710
+ }, options);
711
+ }
506
712
  export {
507
713
  MetroneAuthError,
508
714
  MetroneConfigError,
@@ -513,6 +719,10 @@ export {
513
719
  MetroneServer,
514
720
  MetroneServerError,
515
721
  MetroneTimeoutError,
516
- MetroneValidationError
722
+ MetroneValidationError,
723
+ agentMiddleware,
724
+ captureAgentHit,
725
+ matchAiAgent,
726
+ trackAgentRequest
517
727
  };
518
728
  //# sourceMappingURL=index.js.map