@k-msg/analytics
Analytics and reporting for k-msg, built on top of delivery-tracking records.
Installation
Section titled “Installation”npm install @k-msg/analytics k-msg @k-msg/messaging @k-msg/provider# orbun add @k-msg/analytics k-msg @k-msg/messaging @k-msg/providerFeatures
Section titled “Features”- Query-based (recommended): compute KPIs by reading
DeliveryTrackingStorerecords (SQLite / Bun.SQL / memory) - Breakdowns: by status, provider, message type
- (Experimental) in-memory collectors/insights/reporting utilities (subject to change)
Basic Usage (Query-Based)
Section titled “Basic Usage (Query-Based)”import { KMsg } from "k-msg";import { DeliveryTrackingService, createDeliveryTrackingHooks,} from "@k-msg/messaging/tracking";import { SqliteDeliveryTrackingStore } from "@k-msg/messaging/adapters/bun";import { DeliveryTrackingAnalyticsService } from "@k-msg/analytics";
const providers = [ /* new SolapiProvider(...), new IWINVProvider(...), ... */];
// 1) Tracking (writes to store)const store = new SqliteDeliveryTrackingStore({ dbPath: "./kmsg.sqlite" });const tracking = new DeliveryTrackingService({ providers, store });await tracking.init();
const kmsg = new KMsg({ providers, hooks: createDeliveryTrackingHooks(tracking),});
await kmsg.send({ to: "01012345678", text: "hello" });
// 2) Analytics (reads from the same store)const analytics = new DeliveryTrackingAnalyticsService({ store });const summary = await analytics.getSummary( { requestedAt: { start: new Date(Date.now() - 24 * 60 * 60 * 1000), end: new Date() } }, { includeByProviderId: true, includeByType: true },);
console.log(summary);Using Bun.SQL (Postgres/MySQL/SQLite)
Section titled “Using Bun.SQL (Postgres/MySQL/SQLite)”import { BunSqlDeliveryTrackingStore } from "@k-msg/messaging/adapters/bun";import { DeliveryTrackingAnalyticsService } from "@k-msg/analytics";
const store = new BunSqlDeliveryTrackingStore({ options: { adapter: "postgres", url: process.env.DATABASE_URL!, },});
const analytics = new DeliveryTrackingAnalyticsService({ store });await analytics.init();Webhook collector signatures (experimental)
Section titled “Webhook collector signatures (experimental)”WebhookCollector checks an HMAC-SHA256 signature on each webhook before
collecting it. The check is on by default (enableSignatureValidation: true):
- The constructor throws without a
secretKey. To accept unsigned webhooks, passenableSignatureValidation: false. - The signature is read from the
signatureHeaderheader (defaultx-signature, matched in any case), or fromwebhook.signaturewhen that header is missing. It issha256=followed by the hex HMAC-SHA256 of the raw body, keyed withsecretKey. The prefix is optional, the hex can be in either case, and the digests are compared in constant time. - The raw body is
webhook.rawBody: the request body exactly as received, as a string,Uint8Array, orArrayBuffer. A webhook without it is rejected, becauseJSON.parsefollowed byJSON.stringifyrarely gives back the bytes the sender signed. Prefer the bytes (await request.arrayBuffer(), or Node’s rawBuffer):request.text()drops a leading byte order mark and replaces invalid UTF-8, which changes what was signed. - The collector parses
bodyfrom the verified raw body, which must be UTF-8 JSON, so only signed data reaches the transformers.bodycan be left out; one passed alongside is replaced. maxPayloadSize(default 1 MB) applies to the raw body’s size in bytes before the signature is checked.
import { WebhookCollector } from "@k-msg/analytics";
const collector = new WebhookCollector({ secretKey: process.env.WEBHOOK_SECRET, signatureHeader: "x-hub-signature-256",});
export async function receiveWebhook(request: Request) { // The bytes as received: the collector verifies them, then parses them. const rawBody = await request.arrayBuffer(); // Rejects with "Invalid webhook signature" when the signature does not match. return collector.receiveWebhook({ id: crypto.randomUUID(), source: "sms-provider", timestamp: new Date(), headers: Object.fromEntries(request.headers), rawBody, });}The signature covers only the body, so it does not stop a captured request from being sent again: deduplicate on an id in the body, such as the provider’s message id.
Deliveries from @k-msg/webhook sign <X-Webhook-Timestamp>.<raw body>
rather than the body alone, so this check rejects them. Verify those with
@k-msg/webhook and collect them with enableSignatureValidation: false.
@k-msg/analyticsdoes not create its own database. It reads from thekmsg_delivery_trackingtable written byDeliveryTrackingService.- Tracking SQL schema now disables the
rawcolumn by default (storeRaw: false); enable it in the tracking store only when needed. - For production usage, prefer a durable store (
SqliteDeliveryTrackingStoreorBunSqlDeliveryTrackingStore). - Runtime diagnostics in analytics modules use
@k-msg/corelogger (no directconsole.*in runtime paths).
License
Section titled “License”MIT