@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/README.md +38 -0
- package/dist/index.cjs +217 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -2
- package/dist/index.d.ts +167 -2
- package/dist/index.js +212 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|