{"version":3,"sources":["../src/analytics.ts"],"sourcesContent":["// App analytics — the app-facing surface for `analytics:emit`\n// (APP_ANALYTICS_SPEC §2/§3/§5, roadmap R3-350).\n//\n// ─────────────────────────────────────────────────────────────────────────────\n// WHAT THIS IS FOR, STATED HONESTLY\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// An app runs in an opaque-origin sandboxed iframe where even touching\n// `localStorage` throws. It has no storage and no identity of its own, so **a\n// publisher has no way to learn how their app is used.**\n//\n// The honest argument for this capability is *not* that it displaces `net:fetch` —\n// it does not displace anything, and nothing here narrows or discourages\n// `net:fetch`. The argument is a **comparison of bargains**: publishers have a real\n// need; absent a fitted capability they will request `net:fetch` to satisfy it; and\n// a `net:fetch` grant obtained for analytics is a worse bargain for the user than an\n// analytics grant — broader reach, arbitrary bodies, and a consent line that says\n// \"network access\" rather than \"usage statistics\". An app holding BOTH is displayed\n// as such at consent, because the analytics grant then bounds nothing.\n//\n// ─────────────────────────────────────────────────────────────────────────────\n// WRITE-ONLY. THERE IS NO READ METHOD HERE, AND NONE IS COMING.\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// T-AN-6. An app that could read its own aggregates would learn about the USER —\n// which is the party the architecture assumes is being protected from unaccountable\n// publishers. §13 records the read path as rejected: *\"a plausible feature that\n// converts a write-only channel into a two-way one, and would need its own threat\n// pass.\"* The wire family declares no push channel and no poll, so there is nothing\n// for a read method to ride on.\n//\n// The reply is a bare acknowledgement. It carries no count, no remaining budget, and\n// no \"was that route recognised\" — each of those would be a read channel by another\n// name, and an app that could probe which paths are accepted has one.\n//\n// ─────────────────────────────────────────────────────────────────────────────\n// WHAT YOU MUST DECLARE, AND WHY IT IS THE SHAPE IT IS\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// Everything an app may emit is declared in its manifest and consented to by the\n// user. **The hash of that declaration is bound into the grant** (§2.1): change the\n// vocabulary and the grant is invalid until the user re-consents with the new one\n// shown. `appKey` carries no ref, so without that binding a publisher could observe\n// their aggregates and then ship an alphabet tuned to encode what they now want to\n// read, under a grant given for something else.\n//\n//   \"capabilities\": [{\n//     \"name\": \"analytics:emit\",\n//     \"params\": {\n//       \"events\": {\n//         \"clinic.view\":   { \"props\": { \"tab\": { \"type\": \"enum\", \"values\": [\"summary\", \"meds\"] } } },\n//         \"clinic.export\": { \"props\": { \"format\": { \"type\": \"enum\", \"values\": [\"pdf\", \"csv\"] } } }\n//       },\n//       \"routes\": [\"/patients/:id\", \"/patients\"]\n//     }\n//   }]\n//\n// - **String properties must be bounded ENUMERATIONS**, never free strings; numeric\n//   ones declare an INTEGRAL range. This is the difference between a bound that is\n//   computed and one that is hoped for: analytics is a covert channel with a\n//   capacity, and a free string carries the file, the key, the row. It will be the\n//   first thing you want relaxed; §12 says so in advance.\n// - **The total declared vocabulary is capped** at 2^12 distinct emit-shapes. Eight\n//   properties of sixteen values each looks modest written out and is 4.3 billion.\n// - **Routes are patterns, never paths.** `/patients/12345` is not a page name. You\n//   pass the concrete path to {@link recordRoute} and the platform records\n//   `/patients/:id`; the variable segment is discarded at the boundary and never\n//   transmitted. If you need per-item counts, declare a bounded enumeration of\n//   items — there is no unbounded per-item cardinality by design.\n// - **There is a per-user daily cap**, published in the consent line's detail.\n//\n// ─────────────────────────────────────────────────────────────────────────────\n// WHEN THIS WILL REFUSE\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// Every refusal is a typed error, and none of them is retryable by trying harder:\n//\n// - `forbidden` — no grant; or the grant is bound to a DIFFERENT vocabulary than the\n//   one currently declared (re-consent needed); or the app is running under the M3\n//   stranger stance, which refuses `analytics:*` outright; or the caller is a task\n//   app invoked by a holder — the capability is not delegable.\n// - `invalid-params` — the event is not in the declared vocabulary: an undeclared\n//   name or key, a string outside its enumeration, a number outside its range.\n//   Rejected, never silently stripped.\n// - `budget` — the per-(app, user) daily cap is spent.\n//\n// **Emits never throw into your app by default.** {@link track} swallows refusals,\n// because an analytics call failing is not a reason for a feature to fail. Use\n// {@link emitAnalyticsEvent} when you want the error.\n\nimport { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_ANALYTICS } from './generated/protocol';\n\n/** Property values a declared event may carry. Scalars only — an object or an array\n *  has no bound, and the bound is the point. */\nexport type AnalyticsPropValue = string | number | boolean;\n\n/** The acknowledgement. Deliberately carries nothing but the fact of acceptance —\n *  anything else would be a read channel back into the app (T-AN-6). */\nexport interface AnalyticsAck {\n  accepted: boolean;\n}\n\n/**\n * Emit one declared analytics event, surfacing refusals as typed errors.\n *\n * @param name a declared, app-namespaced event name (`myapp.somethingHappened`).\n *             It may not collide with the host vocabulary.\n * @param props declared properties only. An undeclared key REJECTS the event —\n *              nothing is stripped, because a silently dropped key turns a leak into\n *              a green test.\n *\n * @throws `forbidden` | `invalid-params` | `budget` — see the module header.\n */\nexport const emitAnalyticsEvent = (name: string, props?: Record<string, AnalyticsPropValue>): Promise<AnalyticsAck> =>\n  protocolRequest(SCHEMES[PROTOCOL_ANALYTICS], 'emit', [{ name, ...(props ? { props } : {}) }]);\n\n/**\n * Record a navigation, as a declared route PATTERN.\n *\n * Pass the concrete path your router produced. The platform matches it against your\n * declared patterns and records the **pattern**: `/patients/12345` becomes\n * `/patients/:id`, and the id is discarded at the boundary rather than filtered\n * later. A path matching no declared pattern records nothing — and reports success\n * either way, because telling you which paths are accepted would be a read channel.\n *\n * @throws the same typed errors as {@link emitAnalyticsEvent}.\n */\nexport const recordRoute = (path: string): Promise<AnalyticsAck> =>\n  protocolRequest(SCHEMES[PROTOCOL_ANALYTICS], 'route', [{ path }]);\n\n/**\n * Fire-and-forget {@link emitAnalyticsEvent}.\n *\n * Swallows every refusal, because an analytics call failing is not a reason for a\n * feature to fail — and because the alternative is every call site wrapping this in\n * a `try` that does nothing, which is the same behaviour with more places to get it\n * wrong. Use {@link emitAnalyticsEvent} while you are getting your vocabulary right;\n * use this in the code you ship.\n */\nexport const track = (name: string, props?: Record<string, AnalyticsPropValue>): void => {\n  void emitAnalyticsEvent(name, props).catch(() => {});\n};\n\n/** Fire-and-forget {@link recordRoute}, on the same reasoning as {@link track}. */\nexport const trackRoute = (path: string): void => {\n  void recordRoute(path).catch(() => {});\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0FA,0BAAgC;AAChC,6BAAwB;AACxB,sBAAmC;AAuB5B,MAAM,qBAAqB,CAAC,MAAc,cAC/C,qCAAgB,+BAAQ,kCAAkB,GAAG,QAAQ,CAAC,EAAE,MAAM,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAG,CAAC,CAAC;AAavF,MAAM,cAAc,CAAC,aAC1B,qCAAgB,+BAAQ,kCAAkB,GAAG,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;AAW3D,MAAM,QAAQ,CAAC,MAAc,UAAqD;AACvF,OAAK,mBAAmB,MAAM,KAAK,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACrD;AAGO,MAAM,aAAa,CAAC,SAAuB;AAChD,OAAK,YAAY,IAAI,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACvC;","names":[]}