@molecule/api-analytics-http

Provider bond · analytics · API (Node) · v1.0.1 · Apache-2.0

Generic HTTP analytics provider — POSTs track/identify/page events to a configured first-party endpoint (built for mlcl/molecule-mcp telemetry).

npm install @molecule/api-analytics-http

npm · Source on GitHub · Implements @molecule/api-analytics

How it works

@molecule/api-analytics-http is a provider bond on the API (Node) side: it implements the analytics core interface (@molecule/api-analytics) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider } from '@molecule/api-analytics'
import { createHttpAnalyticsProvider } from '@molecule/api-analytics-http'

setProvider(
  createHttpAnalyticsProvider({
    url: 'https://api.molecule.dev/v1/telemetry/cli',
    // Optional request tagging — defaults to 'unknown'.
    source: 'mlcl',
  }),
)
// track() now POSTs; failures are swallowed.

Reference

Generic HTTP analytics provider.

POSTs each track() / identify() / page() call as a small JSON document to a configured first-party endpoint (the url option or the MOLECULE_ANALYTICS_URL env var). No endpoint is assumed — when none is configured the provider no-ops, so an unconfigured consumer never silently phones home. Every POST is best-effort: network failures are swallowed (logged only via the optional onError hook), so telemetry can never break the CLI or service emitting it.

Built for first-party ingestion — molecule.dev's own tooling (the mlcl CLI, the molecule-mcp server) uses this bond to emit usage telemetry into the platform's existing analytics pipeline, dogfooding the same @molecule/api-analytics interface scaffolded apps consume.

Quick Start

import { setProvider } from '@molecule/api-analytics'
import { createHttpAnalyticsProvider } from '@molecule/api-analytics-http'

setProvider(
  createHttpAnalyticsProvider({
    url: 'https://api.molecule.dev/v1/telemetry/cli',
    // Optional request tagging — defaults to 'unknown'.
    source: 'mlcl',
  }),
)
// track() now POSTs; failures are swallowed.

Type

provider

Installation

npm install @molecule/api-analytics-http @molecule/api-analytics

API

Interfaces

HttpAnalyticsProviderOptions

Options for createHttpAnalyticsProvider.

interface HttpAnalyticsProviderOptions {
  /** Endpoint that receives the POSTs. Defaults to $MOLECULE_ANALYTICS_URL; no-op when neither is set. */
  url?: string
  /** Tag identifying the emitting tool ('mlcl', 'molecule-mcp', …). Defaults to 'unknown'. */
  source?: string
  /** Best-effort error observer (e.g. debug logging). Never throws. */
  onError?: (error: unknown) => void
  /** Abort emit after this many ms (default 5000). */
  timeoutMs?: number
}

Functions

createHttpAnalyticsProvider(options?)

Build an HTTP analytics provider bound to the configured endpoint.

function createHttpAnalyticsProvider(options?: HttpAnalyticsProviderOptions): AnalyticsProvider

Core Interface

Implements @molecule/api-analytics interface.

Injection Notes

Runtime Dependencies

  • @molecule/api-analytics

Wire format — one JSON object per call:

{
  "kind": "track",              // "track" | "identify" | "page" | "group"
  "source": "mlcl",             // the `source` option
  "sentAt": "2026-09-19T…",     // ISO timestamp
  "event": { /* the AnalyticsEvent / user / pageView as given *\/ }
}

The receiving endpoint is expected to validate and allowlist (never trust a remote client's event names) — see molecule-dev's /v1/telemetry/cli route for the reference implementation. POSTs carry a 5-second timeout; a slow or dead endpoint costs at most one aborted request, never a hang.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • Each user action the app cares about (signup/login, a page or screen view, a purchase/conversion or other domain event) EMITS a track() / page() with the RIGHT event name + properties when performed in the UI — and the event is actually RECORDED, proven by reading it back, not by finding a track(...) call in source. Analytics is fire-and-forget to a third-party (Mixpanel/PostHog/Segment) that is NOT reachable in the sandbox and NOT captured by read_activity (that captures email/sms/push/webhook/ channel, never analytics). So prove the wiring locally: read the event back from the app's own events store/dashboard/funnel if it has one, or bond a local recording/console provider for dev and confirm the call fired via read_logs.
  • Events are ATTRIBUTED to the right identity: identify({ userId, ... }) runs at signup/login so events carry that userId. Server-side has no ambient session — a track() / page() with neither userId nor anonymousId collapses every user into one shared "anonymous" identity. If the app tracks activity before login, confirm those anonymousId events associate/merge to the user on identify.
  • Tracking is NON-BLOCKING. Unlike the app-side bond, these SERVER functions PROPAGATE provider failures (they reject when the provider does, and throw when none is bonded), so fire-and-forget is the CALLER's job: every call site wraps the call in .catch() / log-and-continue. Verify by forcing a failure (unbonded or erroring provider) — the user action (signup, checkout) still completes with no visible error and no added latency; the analytics await never blocks the response.
  • PRIVACY — no secret or sensitive PII in event properties or identify traits: never a password, full card/PAN, CVV, auth token, or session cookie. Read the recorded event back and confirm only non-sensitive attributes (plan name, amount, item id) are present.
  • If the app exposes analytics reports/dashboards/funnels, the numbers reflect REAL recorded events (perform an action, the matching count/funnel step increments) and are SCOPED — a user/org reads only its OWN analytics; no endpoint lets one org read another org's numbers (group() associates a user under the bond's group type, 'company' by default).