@docx-editor.dev/telemetry 0.0.1 → 2.29.0

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.
Files changed (47) hide show
  1. package/README.md +85 -7
  2. package/THIRD_PARTY_NOTICES.md +22 -0
  3. package/dist/chunk-73YEPWY5.js +1 -0
  4. package/dist/chunk-7ZXJ6HP2.cjs +1 -0
  5. package/dist/chunk-DT2KMPUJ.js +1 -0
  6. package/dist/chunk-DTEVX2XS.js +1 -0
  7. package/dist/chunk-LFQSY2TD.cjs +1 -0
  8. package/dist/chunk-LFX65QJG.js +4 -0
  9. package/dist/chunk-LGNT4I5W.cjs +1 -0
  10. package/dist/chunk-MJX3ECH3.cjs +4 -0
  11. package/dist/chunk-NUN46EXH.js +2 -0
  12. package/dist/chunk-RRYW7EAV.js +1 -0
  13. package/dist/chunk-S7KBMROA.js +1 -0
  14. package/dist/chunk-U6ERFNUM.cjs +1 -0
  15. package/dist/chunk-UK2MNJCG.cjs +2 -0
  16. package/dist/chunk-WGWKNKUT.cjs +1 -0
  17. package/dist/hocuspocus/index.cjs +1 -0
  18. package/dist/hocuspocus/index.d.cts +118 -0
  19. package/dist/hocuspocus/index.d.ts +118 -0
  20. package/dist/hocuspocus/index.js +1 -0
  21. package/dist/index.cjs +1 -0
  22. package/dist/index.d.cts +332 -0
  23. package/dist/index.d.ts +332 -0
  24. package/dist/index.js +1 -0
  25. package/dist/ingest/index.cjs +1 -0
  26. package/dist/ingest/index.d.cts +107 -0
  27. package/dist/ingest/index.d.ts +107 -0
  28. package/dist/ingest/index.js +1 -0
  29. package/dist/otel/index.cjs +1 -0
  30. package/dist/otel/index.d.cts +57 -0
  31. package/dist/otel/index.d.ts +57 -0
  32. package/dist/otel/index.js +1 -0
  33. package/dist/pipeline-DbVW7S-K.d.cts +223 -0
  34. package/dist/pipeline-DbVW7S-K.d.ts +223 -0
  35. package/dist/prometheus/index.cjs +1 -0
  36. package/dist/prometheus/index.d.cts +88 -0
  37. package/dist/prometheus/index.d.ts +88 -0
  38. package/dist/prometheus/index.js +1 -0
  39. package/dist/sentry/index.cjs +1 -0
  40. package/dist/sentry/index.d.cts +134 -0
  41. package/dist/sentry/index.d.ts +134 -0
  42. package/dist/sentry/index.js +1 -0
  43. package/dist/server/index.cjs +1 -0
  44. package/dist/server/index.d.cts +156 -0
  45. package/dist/server/index.d.ts +156 -0
  46. package/dist/server/index.js +1 -0
  47. package/package.json +100 -16
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export{a as callbackExporter,d as consoleExporter,c as isFailure,e as memoryExporter}from'./chunk-NUN46EXH.js';import {c,b as b$1}from'./chunk-S7KBMROA.js';export{b as ENGINE_VERSION,a as MAX_METRIC_LABEL_VALUES}from'./chunk-S7KBMROA.js';import {a as a$1}from'./chunk-73YEPWY5.js';export{a as METRICS,b as prometheusName}from'./chunk-73YEPWY5.js';import {b,a}from'./chunk-DTEVX2XS.js';import'./chunk-DT2KMPUJ.js';import {explainDiagnostic}from'@docx-editor.dev/core';export{explainDiagnostic}from'@docx-editor.dev/core';var W={documentOpen:true,editing:true,trackedChanges:true,fonts:true,history:true,customNodes:true,collaborationClient:true,performance:true},q=new Set(["edit.tracking-required","edit.suggesting-needs-author","edit.tracking-mode-unsupported","edit.tracking-author-required","edit.tracking-unsupported-host"]);function U(e){switch(e.kind){case "document.open":return "documentOpen";case "edit.rejected":return e.tracked||q.has(e.code)?"trackedChanges":"editing";case "review.skipped":return "trackedChanges";case "font.failed":return "fonts";case "history":return "history";case "custom-node":return "customNodes";case "collab.status":case "collab.participants":return "collaborationClient";case "performance":return "performance";default:return null}}function X(e){return e.kind==="edit.rejected"?[e.editorSessionId,e.code,e.cause,e.errorName,e.stage,e.origin,e.tracked,e.command].join("|"):e.kind==="font.failed"?`${e.editorSessionId}|font|${e.code}`:e.kind==="history"||e.kind==="custom-node"?`${e.editorSessionId}|${e.kind}|${e.code}`:e.kind==="collab.participants"?`${e.editorSessionId}|participants`:e.kind==="collab.status"?`${e.editorSessionId}|status|${e.status}|${e.code??""}|${e.recovering}`:null}function Y(e){let t=c(e),m=e.metricLabels??[];function n(r){let o={};try{let a=typeof e.attributes=="function"?e.attributes():e.attributes;o=b(a??{},"attributes");}catch{}return Object.freeze({...o,...r})}function s(r){let o={};for(let a of m)r[a]!==void 0&&(o[a]=r[a]);return o}let f=e.include??W,l=e.dedupeWindowMs??1e4,g=r=>{try{if(e.filter&&!e.filter(r))return}catch{return}t.event(r);},h=e.documentShape==="detailed",y=new Map,T=false,w=()=>{},E=false;function D(r){let o=f[r];return o?o===true?1:Math.min(1,Math.max(0,o.sampleRate??1)):null}let b$2=new Map;function k(r,o,a){if(a>=1)return true;if(o!=="performance")return Math.random()<a;let p=b$2.get(r.editorSessionId);return p===void 0&&(b$2.size>=1e3&&b$2.clear(),p=Math.random()<a,b$2.set(r.editorSessionId,p)),p}function v(){if(T)return;T=true;let r=globalThis;if(typeof r.addEventListener!="function")return;let o=()=>{t.setPageClosing(true),u();},a=()=>{let p=globalThis.document?.visibilityState==="hidden";t.setPageClosing(p),p&&u();};r.addEventListener("pagehide",o),r.addEventListener("visibilitychange",a),w=()=>{r.removeEventListener?.("pagehide",o),r.removeEventListener?.("visibilitychange",a);};}function x(r){let o=y.get(r);if(o)if(y.delete(r),clearTimeout(o.timer),o.latest){let a=o.event;o.latest.count!==a.count&&g(I(F(o.latest,h),t,o.event.attributes));}else o.repeated>0&&g({...o.event,repeated:o.repeated});}function i(r,o={}){if(E||!t.consented())return;let a$2=n(o),p=a(I(r,t,a$2));if("dropped"in p){t.metric(a$1.droppedEvents,1,{...s(a$2),reason:p.dropped,"docx_editor.version":b$1});return}let A=U(r),M=A?D(A):null;if(M===null||A==="performance"&&M===0)return;v(),Z(t,p.event,s(a$2));let S=l>0?X(r):null,O=S?y.get(S):void 0;if(O){r.kind==="collab.participants"?O.latest=r:O.repeated+=1;return}if(!k(r,A,M))return;let N=I(F(r,h),t,a$2);if(g(N),S){let R=setTimeout(()=>x(S),l);R.unref?.(),y.set(S,{event:N,repeated:0,timer:R});}}function c$1(r){return Object.freeze({id:"telemetry",onDiagnostic:o=>i(o,r),wantsDiagnostic:o=>!E&&(o!=="performance"||(D("performance")??0)>0&&t.consented())})}async function u(){for(let r of [...y.keys()])x(r);await t.flush();}return {module:c$1({}),moduleFor(r){return c$1(b(r,"moduleFor"))},record:i,flush:u,async shutdown(){for(let r of [...y.keys()])x(r);E=true,w(),await t.shutdown();}}}function F(e,t){if(t||e.kind!=="document.open"||e.outcome!=="opened"||!("shape"in e))return e;let{shape:m,...n}=e;return n}function I(e,t,m){let n="code"in e?e.code:void 0;return {...e,...n?explainDiagnostic(n):{},engineVersion:b$1,resource:t.resource,attributes:m}}function Z(e,t,m){let n={metric:(s,f,l={})=>e.metric(s,f,{...l,...m,"docx_editor.version":b$1})};switch(t.kind){case "document.open":t.outcome==="opened"?(n.metric(a$1.documentOpens,1,{outcome:"opened",code:"none",cause:"none",part:"none"}),n.metric(a$1.documentOpenDuration,t.durationMs/1e3)):n.metric(a$1.documentOpens,1,{outcome:"rejected",code:t.code,cause:t.cause??"none",part:t.part??"none"});return;case "edit.rejected":n.metric(a$1.editRejections,1,{code:t.code,cause:t.cause??"none",origin:t.origin,tracked:t.tracked});return;case "review.skipped":n.metric(a$1.reviewSkipped,t.count,{code:t.code,cause:t.cause??"none",operation:t.operation??"none",change_kind:t.changeKind??"none"});return;case "font.failed":n.metric(a$1.fontFailures,1,{code:t.code});return;case "history":n.metric(a$1.historyDiagnostics,1,{code:t.code});return;case "custom-node":n.metric(a$1.customNodeDiagnostics,1,{code:t.code});return;case "collab.status":n.metric(a$1.collabClientStatusTransitions,1,{status:t.status,code:t.code??"none"}),t.connectMs!==void 0&&n.metric(a$1.collabClientConnectDuration,t.connectMs/1e3);return;case "collab.participants":n.metric(a$1.collabClientParticipants,t.count);return;case "performance":{let s=t.pagesBucket??"unknown";L(n,a$1.editorInteractionDuration,s,t.interaction),L(n,a$1.editorLayoutDuration,s,t.layout),L(n,a$1.editorPaintDuration,s,t.paint);}}}function L(e,t,m,n){if(!n)return;let s=t.buckets??[];n.bands.forEach((f,l)=>{let g=s[l],h=l===0?0:s[l-1],y=g===void 0?Math.max(n.maxMs/1e3,h+1e-6):(h+g)/2;for(let T=0;T<f;T+=1)e.metric(t,y,{pages:m});});}var J=new TextEncoder,Q=e=>J.encode(e).byteLength;function ee(e){let t=Math.min(50,Math.max(1,Math.floor(e.batchSize??20))),m=e.flushIntervalMs??1e4,n=[],s,f,l=new Set,g=i=>{try{e.onError?.(i);}catch{}},h=(i,c)=>{if(typeof fetch!="function")return;let u=fetch(e.url,{method:"POST",headers:{"Content-Type":"application/json",...e.headers},body:i,keepalive:c}).then(r=>{if(!r.ok)throw new Error(`telemetry endpoint answered ${r.status}`)}).catch(g).finally(()=>l.delete(u));l.add(u);},y=i=>{let c=globalThis.navigator;if(!e.headers&&typeof c?.sendBeacon=="function")try{if(c.sendBeacon(e.url,new Blob([i],{type:"application/json"})))return}catch{}h(i,true);},T=i=>{let c=Math.min(t,n.length);for(;c>0;){let u=JSON.stringify(n.slice(0,c)),r=Q(u);if(r<=i)return {body:u,bytes:r,count:c};if(c===1)return null;c=Math.ceil(c/2);}return null},w=0,E=false,D=i=>{if(s!==void 0&&clearTimeout(s),s=void 0,f&&!f.consented()){n=[];return}(!i||!E)&&(w=0),E=i;let c=0;for(;n.length;){let u=T(61440-(i?w:0));if(!u){c+=i?n.length:1,n=i?[]:n.slice(1);continue}n=n.slice(u.count),i?(w+=u.bytes,y(u.body)):h(u.body,false);}c&&g(new Error(`telemetry dropped ${c} events that did not fit`));},b=()=>{let i=f?.pageClosing()??false;return i||(E=false),i},k=globalThis.document,v=()=>{k?.visibilityState==="visible"&&(E=false);},x=()=>k?.removeEventListener?.("visibilitychange",v);return {name:"http",attach(i){f=i,k?.addEventListener?.("visibilitychange",v);},event(i){n.push(i),n.length>=t?D(b()):s===void 0&&(s=setTimeout(()=>D(b()),m),s.unref?.());},async flush(){D(b()),await Promise.all(l);},async shutdown(){x(),D(b()),await Promise.all(l);}}}function te(e={}){return ()=>{let t=globalThis;if(!t.window||!t.document||!t.navigator||t.navigator.globalPrivacyControl===true||t.navigator.doNotTrack==="1")return false;if(!e.optOutKey)return true;try{return globalThis.localStorage?.getItem(e.optOutKey)!=="off"}catch{return true}}}export{te as browserPrivacySignals,Y as createTelemetry,ee as httpExporter};
@@ -0,0 +1 @@
1
+ 'use strict';var chunkLFQSY2TD_cjs=require('../chunk-LFQSY2TD.cjs');require('../chunk-WGWKNKUT.cjs');var b=50,i=e=>typeof e=="string",l=e=>typeof e=="number"&&Number.isFinite(e),m=e=>typeof e=="boolean",k={"edit.rejected":[["code",i],["origin",i],["tracked",m]],"review.skipped":[["code",i],["count",l]],"font.failed":[["code",i]],history:[["code",i]],"custom-node":[["code",i]],"collab.status":[["status",i],["previous",i],["recovering",m]],"collab.participants":[["count",l]],performance:[["windowMs",l]]};function h(e){if(!l(e.schemaVersion)||!l(e.time)||!i(e.editorSessionId)||!i(e.engineVersion))return false;if(e.kind==="document.open")return e.outcome==="opened"?i(e.sizeBucket)&&l(e.durationMs):e.outcome==="rejected"&&i(e.code);let r=k[e.kind];return !!r&&r.every(([t,o])=>o(e[t]))}function w(e,r){if(r.size===0)return {};let t=chunkLFQSY2TD_cjs.c(e)??{};return Object.fromEntries(Object.entries(t).filter(([o])=>r.has(o)))}function E(e,r={}){let t=r.maxEvents??b,o=new Set(r.attributeKeys??[]);return !Array.isArray(e)||e.length>t?[]:e.flatMap(n=>{if(!n||typeof n!="object"||Array.isArray(n))return [];let c=n,{resource:d,attributes:u,...s}=c,a=chunkLFQSY2TD_cjs.a(s);if("dropped"in a||!h(a.event))return [];let g=w(c.attributes,o),{attributes:x,...p}=a.event;return [Object.freeze({...p,resource:w(c.resource,o),attributes:g})]})}function O(e){let{hint:r,docs:t,...o}=e,n="code"in e&&e.code?` ${e.code}`:"";return {...o,dt:e.time,message:`${e.kind}${n}`}}var T=64*1024;async function R(e,r){if(Number(e.headers.get("content-length")??0)>r)return null;let t=e.body;if(!t)return "";let o=t.getReader(),n=[],c=0;for(;;){let{done:s,value:a}=await o.read();if(s)break;if(c+=a.byteLength,c>r)return await o.cancel().catch(()=>{}),null;n.push(a);}let d=new Uint8Array(c),u=0;for(let s of n)d.set(s,u),u+=s.byteLength;return new TextDecoder().decode(d)}function v(e){let r=e.maxBodyBytes??T,t=()=>new Response(null,{status:204}),o=n=>{try{e.onError?.(n);}catch{}};return async n=>{if(n.method!=="POST")return t();if(e.allowedOrigins){let s=n.headers.get("origin");if(!s||!e.allowedOrigins.includes(s))return t()}let c;try{c=await R(n,r);}catch{return t()}if(c===null)return t();let d;try{d=JSON.parse(c);}catch{return t()}let u=E(d,e);if(!u.length)return t();try{let a=await e.forward(u);if(a&&a.ok===!1)throw new Error(`the log service answered ${String(a.status)}`)}catch(s){o(s);}return t()}}exports.telemetryRoute=v;exports.toLogRecord=O;exports.validateTelemetryEvents=E;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Receive editor telemetry on your server and forward it to a log service, such as Better Stack,
3
+ * Axiom, or Datadog Logs. Pair it with `httpExporter` in the browser: the browser posts to your
4
+ * route, and only your server holds the log service's token.
5
+ *
6
+ * Anyone can POST to a public route, so every event is checked again with the same closed lists
7
+ * the browser used, and fields the client chose itself are dropped unless you allow them. No
8
+ * dependency: Fetch API `Request` and `Response` only, so it runs in Next.js route handlers,
9
+ * Hono, Bun, Deno, and Cloudflare Workers.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import { a as TelemetryEvent } from '../pipeline-DbVW7S-K.cjs';
14
+ import '@docx-editor.dev/core';
15
+
16
+ /** Options for {@link validateTelemetryEvents}. @public */
17
+ interface ValidateTelemetryEventsOptions {
18
+ /** The most events one request may carry. Defaults to 50; a longer list is refused whole. */
19
+ readonly maxEvents?: number;
20
+ /**
21
+ * Keys of the browser's `resource` and `attributes` to keep, such as `['app.version']`. Each
22
+ * kept value must be a number, a boolean, or an identifier string. Defaults to none: a value
23
+ * the client chose cannot be checked for meaning, so add your fixed fields on the server.
24
+ */
25
+ readonly attributeKeys?: readonly string[];
26
+ }
27
+ /**
28
+ * The telemetry events in an untrusted request body, checked again with the package's own
29
+ * closed lists. An event with an unknown kind or code, or without the fields its kind needs, is
30
+ * dropped. A field outside its list is removed. The client's `resource` and `attributes` keep
31
+ * only the keys in `attributeKeys`. Returns an empty list for a body that is not an array or is
32
+ * longer than `maxEvents`.
33
+ *
34
+ * The check proves that every value comes from the package's lists or is a number. It cannot
35
+ * prove that a client told the truth: a script can send a well-formed event that never
36
+ * happened. It can never send text.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const events = validateTelemetryEvents(await request.json(), { attributeKeys: ['app.version'] });
41
+ * ```
42
+ *
43
+ * @public
44
+ */
45
+ declare function validateTelemetryEvents(body: unknown, options?: ValidateTelemetryEventsOptions): TelemetryEvent[];
46
+ /** A log record with the fields log services read for time and summary. @public */
47
+ type TelemetryLogRecord = Record<string, unknown> & {
48
+ /** The event time in milliseconds since the epoch, which Better Stack and others read. */
49
+ readonly dt: number;
50
+ /** A one-line summary, such as `edit.rejected edit.picture-selected`. */
51
+ readonly message: string;
52
+ };
53
+ /**
54
+ * An event as a log record: `dt` for the time, `message` for the live tail, and every field of
55
+ * the event except the fixed `hint` and `docs` text, which the code already identifies. Add
56
+ * your own fixed fields, such as a service name, to the result.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * body: JSON.stringify(events.map((event) => ({ ...toLogRecord(event), service: 'my-app' })))
61
+ * ```
62
+ *
63
+ * @public
64
+ */
65
+ declare function toLogRecord(event: TelemetryEvent): TelemetryLogRecord;
66
+ /** Options for {@link telemetryRoute}. @public */
67
+ interface TelemetryRouteOptions extends ValidateTelemetryEventsOptions {
68
+ /** Send the checked events on, such as to your log service. Errors are caught and reported. */
69
+ forward(events: readonly TelemetryEvent[]): unknown;
70
+ /** Origins allowed to post, such as `['https://app.example.com']`. Omit to allow any. */
71
+ readonly allowedOrigins?: readonly string[];
72
+ /** The largest body accepted, in bytes. Defaults to 64 KiB. */
73
+ readonly maxBodyBytes?: number;
74
+ /** Called when `forward` throws or rejects, or the log service refuses. Defaults to nothing. */
75
+ readonly onError?: (error: unknown) => void;
76
+ }
77
+ /**
78
+ * A Fetch-style handler for telemetry from `httpExporter`: it accepts POST only, checks the
79
+ * origin, reads at most `maxBodyBytes`, validates every event, and calls `forward` with what
80
+ * passes. It answers 204 to every request, so a telemetry problem never surfaces in the
81
+ * editor; use `onError` to see failures. It does not rate-limit; put it behind your platform's
82
+ * limits.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Next.js: app/api/telemetry/route.ts
87
+ * import { telemetryRoute, toLogRecord } from '@docx-editor.dev/telemetry/ingest';
88
+ *
89
+ * export const POST = telemetryRoute({
90
+ * forward: (events) =>
91
+ * fetch(`https://${process.env.BETTERSTACK_INGESTING_HOST}`, {
92
+ * method: 'POST',
93
+ * headers: {
94
+ * 'Content-Type': 'application/json',
95
+ * Authorization: `Bearer ${process.env.BETTERSTACK_SOURCE_TOKEN}`,
96
+ * },
97
+ * body: JSON.stringify(events.map(toLogRecord)),
98
+ * }),
99
+ * });
100
+ * ```
101
+ *
102
+ * @public
103
+ */
104
+ declare function telemetryRoute(options: TelemetryRouteOptions): (request: Request) => Promise<Response>;
105
+
106
+ export { TelemetryEvent, telemetryRoute, toLogRecord, validateTelemetryEvents };
107
+ export type { TelemetryLogRecord, TelemetryRouteOptions, ValidateTelemetryEventsOptions };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Receive editor telemetry on your server and forward it to a log service, such as Better Stack,
3
+ * Axiom, or Datadog Logs. Pair it with `httpExporter` in the browser: the browser posts to your
4
+ * route, and only your server holds the log service's token.
5
+ *
6
+ * Anyone can POST to a public route, so every event is checked again with the same closed lists
7
+ * the browser used, and fields the client chose itself are dropped unless you allow them. No
8
+ * dependency: Fetch API `Request` and `Response` only, so it runs in Next.js route handlers,
9
+ * Hono, Bun, Deno, and Cloudflare Workers.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import { a as TelemetryEvent } from '../pipeline-DbVW7S-K.js';
14
+ import '@docx-editor.dev/core';
15
+
16
+ /** Options for {@link validateTelemetryEvents}. @public */
17
+ interface ValidateTelemetryEventsOptions {
18
+ /** The most events one request may carry. Defaults to 50; a longer list is refused whole. */
19
+ readonly maxEvents?: number;
20
+ /**
21
+ * Keys of the browser's `resource` and `attributes` to keep, such as `['app.version']`. Each
22
+ * kept value must be a number, a boolean, or an identifier string. Defaults to none: a value
23
+ * the client chose cannot be checked for meaning, so add your fixed fields on the server.
24
+ */
25
+ readonly attributeKeys?: readonly string[];
26
+ }
27
+ /**
28
+ * The telemetry events in an untrusted request body, checked again with the package's own
29
+ * closed lists. An event with an unknown kind or code, or without the fields its kind needs, is
30
+ * dropped. A field outside its list is removed. The client's `resource` and `attributes` keep
31
+ * only the keys in `attributeKeys`. Returns an empty list for a body that is not an array or is
32
+ * longer than `maxEvents`.
33
+ *
34
+ * The check proves that every value comes from the package's lists or is a number. It cannot
35
+ * prove that a client told the truth: a script can send a well-formed event that never
36
+ * happened. It can never send text.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const events = validateTelemetryEvents(await request.json(), { attributeKeys: ['app.version'] });
41
+ * ```
42
+ *
43
+ * @public
44
+ */
45
+ declare function validateTelemetryEvents(body: unknown, options?: ValidateTelemetryEventsOptions): TelemetryEvent[];
46
+ /** A log record with the fields log services read for time and summary. @public */
47
+ type TelemetryLogRecord = Record<string, unknown> & {
48
+ /** The event time in milliseconds since the epoch, which Better Stack and others read. */
49
+ readonly dt: number;
50
+ /** A one-line summary, such as `edit.rejected edit.picture-selected`. */
51
+ readonly message: string;
52
+ };
53
+ /**
54
+ * An event as a log record: `dt` for the time, `message` for the live tail, and every field of
55
+ * the event except the fixed `hint` and `docs` text, which the code already identifies. Add
56
+ * your own fixed fields, such as a service name, to the result.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * body: JSON.stringify(events.map((event) => ({ ...toLogRecord(event), service: 'my-app' })))
61
+ * ```
62
+ *
63
+ * @public
64
+ */
65
+ declare function toLogRecord(event: TelemetryEvent): TelemetryLogRecord;
66
+ /** Options for {@link telemetryRoute}. @public */
67
+ interface TelemetryRouteOptions extends ValidateTelemetryEventsOptions {
68
+ /** Send the checked events on, such as to your log service. Errors are caught and reported. */
69
+ forward(events: readonly TelemetryEvent[]): unknown;
70
+ /** Origins allowed to post, such as `['https://app.example.com']`. Omit to allow any. */
71
+ readonly allowedOrigins?: readonly string[];
72
+ /** The largest body accepted, in bytes. Defaults to 64 KiB. */
73
+ readonly maxBodyBytes?: number;
74
+ /** Called when `forward` throws or rejects, or the log service refuses. Defaults to nothing. */
75
+ readonly onError?: (error: unknown) => void;
76
+ }
77
+ /**
78
+ * A Fetch-style handler for telemetry from `httpExporter`: it accepts POST only, checks the
79
+ * origin, reads at most `maxBodyBytes`, validates every event, and calls `forward` with what
80
+ * passes. It answers 204 to every request, so a telemetry problem never surfaces in the
81
+ * editor; use `onError` to see failures. It does not rate-limit; put it behind your platform's
82
+ * limits.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Next.js: app/api/telemetry/route.ts
87
+ * import { telemetryRoute, toLogRecord } from '@docx-editor.dev/telemetry/ingest';
88
+ *
89
+ * export const POST = telemetryRoute({
90
+ * forward: (events) =>
91
+ * fetch(`https://${process.env.BETTERSTACK_INGESTING_HOST}`, {
92
+ * method: 'POST',
93
+ * headers: {
94
+ * 'Content-Type': 'application/json',
95
+ * Authorization: `Bearer ${process.env.BETTERSTACK_SOURCE_TOKEN}`,
96
+ * },
97
+ * body: JSON.stringify(events.map(toLogRecord)),
98
+ * }),
99
+ * });
100
+ * ```
101
+ *
102
+ * @public
103
+ */
104
+ declare function telemetryRoute(options: TelemetryRouteOptions): (request: Request) => Promise<Response>;
105
+
106
+ export { TelemetryEvent, telemetryRoute, toLogRecord, validateTelemetryEvents };
107
+ export type { TelemetryLogRecord, TelemetryRouteOptions, ValidateTelemetryEventsOptions };
@@ -0,0 +1 @@
1
+ import {a,c}from'../chunk-DTEVX2XS.js';import'../chunk-DT2KMPUJ.js';var b=50,i=e=>typeof e=="string",l=e=>typeof e=="number"&&Number.isFinite(e),m=e=>typeof e=="boolean",k={"edit.rejected":[["code",i],["origin",i],["tracked",m]],"review.skipped":[["code",i],["count",l]],"font.failed":[["code",i]],history:[["code",i]],"custom-node":[["code",i]],"collab.status":[["status",i],["previous",i],["recovering",m]],"collab.participants":[["count",l]],performance:[["windowMs",l]]};function h(e){if(!l(e.schemaVersion)||!l(e.time)||!i(e.editorSessionId)||!i(e.engineVersion))return false;if(e.kind==="document.open")return e.outcome==="opened"?i(e.sizeBucket)&&l(e.durationMs):e.outcome==="rejected"&&i(e.code);let r=k[e.kind];return !!r&&r.every(([t,o])=>o(e[t]))}function w(e,r){if(r.size===0)return {};let t=c(e)??{};return Object.fromEntries(Object.entries(t).filter(([o])=>r.has(o)))}function E(e,r={}){let t=r.maxEvents??b,o=new Set(r.attributeKeys??[]);return !Array.isArray(e)||e.length>t?[]:e.flatMap(n=>{if(!n||typeof n!="object"||Array.isArray(n))return [];let c=n,{resource:d,attributes:u,...s}=c,a$1=a(s);if("dropped"in a$1||!h(a$1.event))return [];let g=w(c.attributes,o),{attributes:x,...p}=a$1.event;return [Object.freeze({...p,resource:w(c.resource,o),attributes:g})]})}function O(e){let{hint:r,docs:t,...o}=e,n="code"in e&&e.code?` ${e.code}`:"";return {...o,dt:e.time,message:`${e.kind}${n}`}}var T=64*1024;async function R(e,r){if(Number(e.headers.get("content-length")??0)>r)return null;let t=e.body;if(!t)return "";let o=t.getReader(),n=[],c=0;for(;;){let{done:s,value:a}=await o.read();if(s)break;if(c+=a.byteLength,c>r)return await o.cancel().catch(()=>{}),null;n.push(a);}let d=new Uint8Array(c),u=0;for(let s of n)d.set(s,u),u+=s.byteLength;return new TextDecoder().decode(d)}function v(e){let r=e.maxBodyBytes??T,t=()=>new Response(null,{status:204}),o=n=>{try{e.onError?.(n);}catch{}};return async n=>{if(n.method!=="POST")return t();if(e.allowedOrigins){let s=n.headers.get("origin");if(!s||!e.allowedOrigins.includes(s))return t()}let c;try{c=await R(n,r);}catch{return t()}if(c===null)return t();let d;try{d=JSON.parse(c);}catch{return t()}let u=E(d,e);if(!u.length)return t();try{let a=await e.forward(u);if(a&&a.ok===!1)throw new Error(`the log service answered ${String(a.status)}`)}catch(s){o(s);}return t()}}export{v as telemetryRoute,O as toLogRecord,E as validateTelemetryEvents};
@@ -0,0 +1 @@
1
+ 'use strict';var chunkUK2MNJCG_cjs=require('../chunk-UK2MNJCG.cjs'),chunkWGWKNKUT_cjs=require('../chunk-WGWKNKUT.cjs'),api=require('@opentelemetry/api');var w="@docx-editor.dev/telemetry";function O(o){let n=o.getDelegate?.()??o;return /^Noop/.test(n?.constructor?.name??"")}var M=false;function E(o,n){let s=chunkWGWKNKUT_cjs.a();M||s==="production"||!O(o)||!O(n)||(M=true,console.warn("[docx-editor telemetry] otelExporter found no OpenTelemetry SDK. Register a MeterProvider and a TracerProvider before creating the exporter, or pass them as options. Until then every measurement is dropped."));}function h(o,n,s){return o.createObservableGauge(n.name,{unit:n.unit,description:n.description}).addCallback(t=>{for(let{value:i,attributes:a}of s.values())t.observe(i,a);}),{record(t,i={}){s.set(f(i),{value:t,attributes:i});},forget(t){s.delete(f(t));}}}function f(o){return JSON.stringify(Object.keys(o).sort().map(n=>[n,o[n]]))}function m(o){let n={...o.resource,...o.attributes};for(let[s,t]of Object.entries(o))if(!(s==="resource"||s==="attributes"||s==="hint"||s==="docs")){if(typeof t=="string"||typeof t=="number"||typeof t=="boolean")n[`docx_editor.${s}`]=t;else if(s==="shape"&&t&&typeof t=="object")for(let[i,a]of Object.entries(t))typeof a=="string"&&(n[`docx_editor.shape.${i}`]=a);else if(t&&typeof t=="object"&&!Array.isArray(t))for(let[i,a]of Object.entries(t))typeof a=="number"&&(n[`docx_editor.${s}.${i}`]=a);}return n}function j(o={}){let n=o.scope??w,s=()=>o.meterProvider??api.metrics.getMeterProvider(),t=()=>o.tracerProvider??api.trace.getTracerProvider(),i,a,l=new Map,p=new Map,g=false;function P(e){let r=p.get(e);return r||(r=new Map,p.set(e,r)),r}function k(){let e=s();return (e!==i||!a)&&(i=e,a=e.getMeter(n),l.clear()),a}function T(e){let r=k();g||(g=true,setTimeout(()=>E(s(),t()),5e3).unref?.());let c=l.get(e.name);if(c)return c;let d={unit:e.unit,description:e.description},u=e.kind==="counter"?r.createCounter(e.name,d):e.kind==="histogram"?r.createHistogram(e.name,{...d,...e.buckets?{advice:{explicitBucketBoundaries:[...e.buckets]}}:{}}):h(r,e,P(e.name));return l.set(e.name,u),u}return {name:"otel",metric(e){let r=T(e.definition),c=e.attributes;"add"in r?r.add(e.value,c):r.record(e.value,c);},event(e){if(e.kind==="document.open"){let d=e.time-(e.outcome==="opened"?e.durationMs:0),u=t().getTracer(n).startSpan("docx_editor.document.open",{startTime:d,attributes:m(e)},o.parentContext?.());e.outcome==="rejected"&&u.setStatus({code:api.SpanStatusCode.ERROR,message:e.code}),u.end(e.time);return}let r=api.trace.getActiveSpan();if(r){r.addEvent(`docx_editor.${e.kind}`,m(e));return}if(!chunkUK2MNJCG_cjs.c(e))return;let c=t().getTracer(n).startSpan(`docx_editor.${e.kind}`,{startTime:e.time,attributes:m(e)},o.parentContext?.());c.setStatus({code:api.SpanStatusCode.ERROR,message:"code"in e?e.code:e.kind}),c.end(e.time);},forget(e,r){p.get(e.name)?.delete(f(r));},async flush(){let e=r=>(r.getDelegate?.()??r).forceFlush?.();await Promise.all([e(s()),e(t())]);}}}exports.otelExporter=j;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OpenTelemetry: metrics and traces through the OpenTelemetry SDK you already run. This entry
3
+ * depends only on `@opentelemetry/api`; your SDK decides where data goes (OTLP to a
4
+ * collector, Datadog, Grafana, Honeycomb, New Relic, and others).
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import { MeterProvider, TracerProvider, Context } from '@opentelemetry/api';
9
+ import { b as TelemetryExporter } from '../pipeline-DbVW7S-K.cjs';
10
+ import '@docx-editor.dev/core';
11
+
12
+ /**
13
+ * OpenTelemetry: metrics and traces through the OpenTelemetry SDK you already run. This entry
14
+ * depends only on `@opentelemetry/api`; your SDK decides where data goes (OTLP to a
15
+ * collector, Datadog, Grafana, Honeycomb, New Relic, and others).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ /** Options for {@link otelExporter}. @public */
21
+ interface OtelExporterOptions {
22
+ /** Defaults to the global provider your SDK registered. */
23
+ readonly meterProvider?: MeterProvider;
24
+ /** Defaults to the global provider your SDK registered. */
25
+ readonly tracerProvider?: TracerProvider;
26
+ /**
27
+ * The parent of each `docx_editor.document.open` span. Browsers have no async context, so
28
+ * without this the span is a root span. Return your page-load or route span's context.
29
+ */
30
+ readonly parentContext?: () => Context | undefined;
31
+ /** Instrumentation scope name. Defaults to `@docx-editor.dev/telemetry`. */
32
+ readonly scope?: string;
33
+ }
34
+ /**
35
+ * Record metrics and traces with OpenTelemetry. The exporter reads the global providers at
36
+ * each use, so it can be created before your SDK starts.
37
+ *
38
+ * Metrics use the names in `METRICS`. Each document open becomes a
39
+ * `docx_editor.document.open` span, with an error status when it is refused. Other events
40
+ * become events on the active span; without one, a failure gets a short span of its own.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createTelemetry } from '@docx-editor.dev/telemetry';
45
+ * import { otelExporter } from '@docx-editor.dev/telemetry/otel';
46
+ *
47
+ * // Before or after your OpenTelemetry SDK starts: the exporter reads the global providers at
48
+ * // each use.
49
+ * export const telemetry = createTelemetry({ exporters: [otelExporter()] });
50
+ * ```
51
+ *
52
+ * @public
53
+ */
54
+ declare function otelExporter(options?: OtelExporterOptions): TelemetryExporter;
55
+
56
+ export { TelemetryExporter, otelExporter };
57
+ export type { OtelExporterOptions };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OpenTelemetry: metrics and traces through the OpenTelemetry SDK you already run. This entry
3
+ * depends only on `@opentelemetry/api`; your SDK decides where data goes (OTLP to a
4
+ * collector, Datadog, Grafana, Honeycomb, New Relic, and others).
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import { MeterProvider, TracerProvider, Context } from '@opentelemetry/api';
9
+ import { b as TelemetryExporter } from '../pipeline-DbVW7S-K.js';
10
+ import '@docx-editor.dev/core';
11
+
12
+ /**
13
+ * OpenTelemetry: metrics and traces through the OpenTelemetry SDK you already run. This entry
14
+ * depends only on `@opentelemetry/api`; your SDK decides where data goes (OTLP to a
15
+ * collector, Datadog, Grafana, Honeycomb, New Relic, and others).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ /** Options for {@link otelExporter}. @public */
21
+ interface OtelExporterOptions {
22
+ /** Defaults to the global provider your SDK registered. */
23
+ readonly meterProvider?: MeterProvider;
24
+ /** Defaults to the global provider your SDK registered. */
25
+ readonly tracerProvider?: TracerProvider;
26
+ /**
27
+ * The parent of each `docx_editor.document.open` span. Browsers have no async context, so
28
+ * without this the span is a root span. Return your page-load or route span's context.
29
+ */
30
+ readonly parentContext?: () => Context | undefined;
31
+ /** Instrumentation scope name. Defaults to `@docx-editor.dev/telemetry`. */
32
+ readonly scope?: string;
33
+ }
34
+ /**
35
+ * Record metrics and traces with OpenTelemetry. The exporter reads the global providers at
36
+ * each use, so it can be created before your SDK starts.
37
+ *
38
+ * Metrics use the names in `METRICS`. Each document open becomes a
39
+ * `docx_editor.document.open` span, with an error status when it is refused. Other events
40
+ * become events on the active span; without one, a failure gets a short span of its own.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createTelemetry } from '@docx-editor.dev/telemetry';
45
+ * import { otelExporter } from '@docx-editor.dev/telemetry/otel';
46
+ *
47
+ * // Before or after your OpenTelemetry SDK starts: the exporter reads the global providers at
48
+ * // each use.
49
+ * export const telemetry = createTelemetry({ exporters: [otelExporter()] });
50
+ * ```
51
+ *
52
+ * @public
53
+ */
54
+ declare function otelExporter(options?: OtelExporterOptions): TelemetryExporter;
55
+
56
+ export { TelemetryExporter, otelExporter };
57
+ export type { OtelExporterOptions };
@@ -0,0 +1 @@
1
+ import {c}from'../chunk-NUN46EXH.js';import {a}from'../chunk-DT2KMPUJ.js';import {SpanStatusCode,trace,metrics}from'@opentelemetry/api';var w="@docx-editor.dev/telemetry";function O(o){let n=o.getDelegate?.()??o;return /^Noop/.test(n?.constructor?.name??"")}var M=false;function E(o,n){let s=a();M||s==="production"||!O(o)||!O(n)||(M=true,console.warn("[docx-editor telemetry] otelExporter found no OpenTelemetry SDK. Register a MeterProvider and a TracerProvider before creating the exporter, or pass them as options. Until then every measurement is dropped."));}function h(o,n,s){return o.createObservableGauge(n.name,{unit:n.unit,description:n.description}).addCallback(t=>{for(let{value:i,attributes:a}of s.values())t.observe(i,a);}),{record(t,i={}){s.set(f(i),{value:t,attributes:i});},forget(t){s.delete(f(t));}}}function f(o){return JSON.stringify(Object.keys(o).sort().map(n=>[n,o[n]]))}function m(o){let n={...o.resource,...o.attributes};for(let[s,t]of Object.entries(o))if(!(s==="resource"||s==="attributes"||s==="hint"||s==="docs")){if(typeof t=="string"||typeof t=="number"||typeof t=="boolean")n[`docx_editor.${s}`]=t;else if(s==="shape"&&t&&typeof t=="object")for(let[i,a]of Object.entries(t))typeof a=="string"&&(n[`docx_editor.shape.${i}`]=a);else if(t&&typeof t=="object"&&!Array.isArray(t))for(let[i,a]of Object.entries(t))typeof a=="number"&&(n[`docx_editor.${s}.${i}`]=a);}return n}function j(o={}){let n=o.scope??w,s=()=>o.meterProvider??metrics.getMeterProvider(),t=()=>o.tracerProvider??trace.getTracerProvider(),i,a,l=new Map,p=new Map,g=false;function P(e){let r=p.get(e);return r||(r=new Map,p.set(e,r)),r}function k(){let e=s();return (e!==i||!a)&&(i=e,a=e.getMeter(n),l.clear()),a}function T(e){let r=k();g||(g=true,setTimeout(()=>E(s(),t()),5e3).unref?.());let c=l.get(e.name);if(c)return c;let d={unit:e.unit,description:e.description},u=e.kind==="counter"?r.createCounter(e.name,d):e.kind==="histogram"?r.createHistogram(e.name,{...d,...e.buckets?{advice:{explicitBucketBoundaries:[...e.buckets]}}:{}}):h(r,e,P(e.name));return l.set(e.name,u),u}return {name:"otel",metric(e){let r=T(e.definition),c=e.attributes;"add"in r?r.add(e.value,c):r.record(e.value,c);},event(e){if(e.kind==="document.open"){let d=e.time-(e.outcome==="opened"?e.durationMs:0),u=t().getTracer(n).startSpan("docx_editor.document.open",{startTime:d,attributes:m(e)},o.parentContext?.());e.outcome==="rejected"&&u.setStatus({code:SpanStatusCode.ERROR,message:e.code}),u.end(e.time);return}let r=trace.getActiveSpan();if(r){r.addEvent(`docx_editor.${e.kind}`,m(e));return}if(!c(e))return;let c$1=t().getTracer(n).startSpan(`docx_editor.${e.kind}`,{startTime:e.time,attributes:m(e)},o.parentContext?.());c$1.setStatus({code:SpanStatusCode.ERROR,message:"code"in e?e.code:e.kind}),c$1.end(e.time);},forget(e,r){p.get(e.name)?.delete(f(r));},async flush(){let e=r=>(r.getDelegate?.()??r).forceFlush?.();await Promise.all([e(s()),e(t())]);}}}export{j as otelExporter};
@@ -0,0 +1,223 @@
1
+ import { EditorDiagnostic } from '@docx-editor.dev/core';
2
+
3
+ /**
4
+ * The metric catalog: one definition per metric, in OpenTelemetry terms. Prometheus names are
5
+ * derived from it mechanically by {@link prometheusName}, so the two outputs cannot drift.
6
+ *
7
+ * Every label is a bounded enum. No metric carries a room name, a user, or a document.
8
+ */
9
+ /** How a metric aggregates. @public */
10
+ type MetricKind = 'counter' | 'gauge' | 'histogram';
11
+ /** One metric in the catalog. @public */
12
+ interface MetricDefinition {
13
+ /** OpenTelemetry name, such as `docx_editor.collab.room.load.duration`. */
14
+ readonly name: string;
15
+ readonly kind: MetricKind;
16
+ /** UCUM unit as OpenTelemetry writes it: `s`, `By`, `1`, or a `{annotation}`. */
17
+ readonly unit: string;
18
+ readonly description: string;
19
+ /** The label keys this metric may carry. Each value is a bounded enum. */
20
+ readonly labels: readonly string[];
21
+ /** Histogram bucket upper bounds, in the metric's unit. */
22
+ readonly buckets?: readonly number[];
23
+ }
24
+ /**
25
+ * Every metric the package records.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { METRICS, prometheusName } from '@docx-editor.dev/telemetry';
30
+ * prometheusName(METRICS.roomLoadDuration); // 'docx_editor_collab_room_load_duration_seconds'
31
+ * ```
32
+ *
33
+ * @public
34
+ */
35
+ declare const METRICS: Readonly<{
36
+ documentOpens: MetricDefinition;
37
+ documentOpenDuration: MetricDefinition;
38
+ editRejections: MetricDefinition;
39
+ reviewSkipped: MetricDefinition;
40
+ fontFailures: MetricDefinition;
41
+ historyDiagnostics: MetricDefinition;
42
+ customNodeDiagnostics: MetricDefinition;
43
+ collabClientStatusTransitions: MetricDefinition;
44
+ collabClientConnectDuration: MetricDefinition;
45
+ collabClientParticipants: MetricDefinition;
46
+ editorInteractionDuration: MetricDefinition;
47
+ editorLayoutDuration: MetricDefinition;
48
+ editorPaintDuration: MetricDefinition;
49
+ droppedEvents: MetricDefinition;
50
+ roomsActive: MetricDefinition;
51
+ connectionsActive: MetricDefinition;
52
+ participantsActive: MetricDefinition;
53
+ roomParticipants: MetricDefinition;
54
+ roomParticipantsMax: MetricDefinition;
55
+ roomMemory: MetricDefinition;
56
+ roomsMemory: MetricDefinition;
57
+ roomNodes: MetricDefinition;
58
+ roomResourceUsage: MetricDefinition;
59
+ roomAge: MetricDefinition;
60
+ roomIdle: MetricDefinition;
61
+ roomLoadDuration: MetricDefinition;
62
+ roomStoreDuration: MetricDefinition;
63
+ admissionRejections: MetricDefinition;
64
+ messageRefusals: MetricDefinition;
65
+ updates: MetricDefinition;
66
+ updateBytes: MetricDefinition;
67
+ roomParticipantsByRoom: MetricDefinition;
68
+ roomMemoryByRoom: MetricDefinition;
69
+ migrations: MetricDefinition;
70
+ }>;
71
+ /** A key of {@link METRICS}. @public */
72
+ type MetricId = keyof typeof METRICS;
73
+ /**
74
+ * The Prometheus name of a metric: dots become underscores, the unit becomes a suffix, and a
75
+ * counter ends in `_total`, as the OpenTelemetry Prometheus exporter writes it.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * prometheusName(METRICS.editRejections); // 'docx_editor_edit_rejections_total'
80
+ * prometheusName(METRICS.editorInteractionDuration); // 'docx_editor_editor_interaction_duration_seconds'
81
+ * ```
82
+ *
83
+ * @public
84
+ */
85
+ declare function prometheusName(definition: MetricDefinition): string;
86
+
87
+ /**
88
+ * The shared delivery path: consent, the anonymity guard, and exporters that can never throw
89
+ * into the code they observe. The editor client and the room server both send through it.
90
+ */
91
+
92
+ /** A metric attribute or a resource value. @public */
93
+ type AttributeValue = string | number | boolean;
94
+ /** Attributes or resource values, keyed by name. @public */
95
+ type Attributes = Readonly<Record<string, AttributeValue>>;
96
+ /** Context the package adds to every diagnostic before it reaches an exporter. @public */
97
+ interface TelemetryEventContext {
98
+ /** The fixed hint for the event's code. A constant, never document content. */
99
+ readonly hint?: string;
100
+ /** The documentation section for the event's code. */
101
+ readonly docs?: string;
102
+ /** The version of the editor packages. Core and this package release together. */
103
+ readonly engineVersion: string;
104
+ /**
105
+ * Present only on a summary: the event that closes a deduplication window repeats the first
106
+ * one, with the number of identical events folded into it beyond the first. Count it, do not
107
+ * treat it as a new failure. Its `time` is the first event's. The Sentry exporter records
108
+ * summaries as breadcrumbs.
109
+ */
110
+ readonly repeated?: number;
111
+ /**
112
+ * The host's own values from `createTelemetry({ resource })`, such as `service.name`. The
113
+ * host decides what goes here, and every event carries it.
114
+ */
115
+ readonly resource: Attributes;
116
+ /**
117
+ * Your identifiers for this event: the client's `attributes`, then the values of the
118
+ * `moduleFor` module of the editor that sent it, such as a room, a document, or a user.
119
+ * Empty when you set none. The engine never adds a value here.
120
+ */
121
+ readonly attributes: Attributes;
122
+ }
123
+ /** A diagnostic as an exporter receives it. Switch on `kind` to narrow it. @public */
124
+ type TelemetryEvent = EditorDiagnostic & TelemetryEventContext;
125
+ /** One metric measurement. @public */
126
+ interface MetricRecord {
127
+ readonly definition: MetricDefinition;
128
+ /** A counter increment, a gauge value, or a histogram sample, in the metric's unit. */
129
+ readonly value: number;
130
+ readonly attributes: Attributes;
131
+ }
132
+ /**
133
+ * Where telemetry goes. Implement `event`, `metric`, or both.
134
+ *
135
+ * An exporter never throws into the editor or the server: a failure is caught and reported
136
+ * once through `onExporterError`.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const toMyLogger: TelemetryExporter = {
141
+ * name: 'my-logger',
142
+ * event: (event) => myLogger.warn(event.kind, { code: 'code' in event ? event.code : undefined }),
143
+ * };
144
+ * ```
145
+ *
146
+ * @public
147
+ */
148
+ interface TelemetryExporter {
149
+ /** Shown in `onExporterError` reports. */
150
+ readonly name: string;
151
+ event?(event: TelemetryEvent): void;
152
+ metric?(record: MetricRecord): void;
153
+ /**
154
+ * Drop a series that no longer exists, such as a closed room's per-room gauge. Optional:
155
+ * an exporter without it keeps the series until it expires on its own.
156
+ */
157
+ forget?(definition: MetricDefinition, attributes: Attributes): void;
158
+ /**
159
+ * Start a series at zero before its first measurement. A scraper that sees a counter first
160
+ * at 1 cannot count that first increase, so the room server declares its known series when
161
+ * it starts. Optional: an exporter without it creates each series at its first measurement.
162
+ */
163
+ declare?(definition: MetricDefinition, attributes: Attributes): void;
164
+ /** Send anything buffered. Called by `flush()` and when a page is hidden. */
165
+ flush?(): void | Promise<void>;
166
+ /** Flush and release resources. Called by `shutdown()`. */
167
+ shutdown?(): void | Promise<void>;
168
+ /**
169
+ * Called once when a telemetry client or room server takes the exporter. An exporter that
170
+ * holds events, such as a batching one, reads `consented()` before it sends, so events queued
171
+ * before consent was withdrawn are discarded, not sent.
172
+ */
173
+ attach?(context: TelemetryExporterContext): void;
174
+ }
175
+ /** What a telemetry client tells an exporter it takes. @public */
176
+ interface TelemetryExporterContext {
177
+ /** Whether the host's consent is given, read now. */
178
+ consented(): boolean;
179
+ /**
180
+ * Whether the page is hidden or closing, read now. While it is, a browser exporter sends with
181
+ * `sendBeacon` or `keepalive`, which still deliver after the page goes away.
182
+ */
183
+ pageClosing(): boolean;
184
+ }
185
+ /** Options shared by the editor client and the room server. @public */
186
+ interface TelemetryPipelineOptions {
187
+ /** Where telemetry goes. Nothing is recorded or sent without at least one exporter. */
188
+ readonly exporters: readonly TelemetryExporter[];
189
+ /**
190
+ * Whether the viewer agreed to telemetry. Read before each event and metric, so a consent
191
+ * banner can change it at any time. Events before consent are dropped, not buffered.
192
+ * Defaults to `true`: without this option, events are sent. Pass it whenever your app asks
193
+ * for consent.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * consent: () => window.cookieConsent?.analytics === true
198
+ * ```
199
+ */
200
+ readonly consent?: boolean | (() => boolean);
201
+ /**
202
+ * Values added to every event, such as `{ 'service.name': 'contracts-app' }`. Metrics do not
203
+ * carry them: set resource attributes on your OpenTelemetry SDK or Prometheus target instead.
204
+ * You own these values: do not put a user, a document, or a room name here.
205
+ */
206
+ readonly resource?: Attributes;
207
+ /** Told once per exporter when it throws. Defaults to one `console.warn`. */
208
+ readonly onExporterError?: (error: unknown, exporter: string) => void;
209
+ /**
210
+ * Keys of your own attributes that may also label metrics, such as `tenant.tier` or
211
+ * `app.region`. Only for values with few distinct values: each value makes a new series.
212
+ * After {@link MAX_METRIC_LABEL_VALUES} distinct values of one key, later values become
213
+ * `other`. Never list a user, document, or room id here; put those on events instead.
214
+ */
215
+ readonly metricLabels?: readonly string[];
216
+ }
217
+ /** Distinct values one of your metric labels may take before later values become `other`. @public */
218
+ declare const MAX_METRIC_LABEL_VALUES = 50;
219
+ /** The version of the editor packages, or `'development'` when running from source. @public */
220
+ declare const ENGINE_VERSION: string;
221
+
222
+ export { ENGINE_VERSION as E, MAX_METRIC_LABEL_VALUES as d, METRICS as e, prometheusName as p };
223
+ export type { Attributes as A, MetricRecord as M, TelemetryPipelineOptions as T, TelemetryEvent as a, TelemetryExporter as b, AttributeValue as c, MetricDefinition as f, MetricId as g, MetricKind as h, TelemetryEventContext as i, TelemetryExporterContext as j };