@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.
- package/README.md +85 -7
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/dist/chunk-73YEPWY5.js +1 -0
- package/dist/chunk-7ZXJ6HP2.cjs +1 -0
- package/dist/chunk-DT2KMPUJ.js +1 -0
- package/dist/chunk-DTEVX2XS.js +1 -0
- package/dist/chunk-LFQSY2TD.cjs +1 -0
- package/dist/chunk-LFX65QJG.js +4 -0
- package/dist/chunk-LGNT4I5W.cjs +1 -0
- package/dist/chunk-MJX3ECH3.cjs +4 -0
- package/dist/chunk-NUN46EXH.js +2 -0
- package/dist/chunk-RRYW7EAV.js +1 -0
- package/dist/chunk-S7KBMROA.js +1 -0
- package/dist/chunk-U6ERFNUM.cjs +1 -0
- package/dist/chunk-UK2MNJCG.cjs +2 -0
- package/dist/chunk-WGWKNKUT.cjs +1 -0
- package/dist/hocuspocus/index.cjs +1 -0
- package/dist/hocuspocus/index.d.cts +118 -0
- package/dist/hocuspocus/index.d.ts +118 -0
- package/dist/hocuspocus/index.js +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +332 -0
- package/dist/index.d.ts +332 -0
- package/dist/index.js +1 -0
- package/dist/ingest/index.cjs +1 -0
- package/dist/ingest/index.d.cts +107 -0
- package/dist/ingest/index.d.ts +107 -0
- package/dist/ingest/index.js +1 -0
- package/dist/otel/index.cjs +1 -0
- package/dist/otel/index.d.cts +57 -0
- package/dist/otel/index.d.ts +57 -0
- package/dist/otel/index.js +1 -0
- package/dist/pipeline-DbVW7S-K.d.cts +223 -0
- package/dist/pipeline-DbVW7S-K.d.ts +223 -0
- package/dist/prometheus/index.cjs +1 -0
- package/dist/prometheus/index.d.cts +88 -0
- package/dist/prometheus/index.d.ts +88 -0
- package/dist/prometheus/index.js +1 -0
- package/dist/sentry/index.cjs +1 -0
- package/dist/sentry/index.d.cts +134 -0
- package/dist/sentry/index.d.ts +134 -0
- package/dist/sentry/index.js +1 -0
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.d.cts +156 -0
- package/dist/server/index.d.ts +156 -0
- package/dist/server/index.js +1 -0
- 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 };
|