@docx-editor.dev/telemetry/hocuspocus

Hocuspocus: room telemetry in one call. It adds one extension that runs before all others and times each load and store from the first hook to the last, and it observes your hooks and the extensions you pass in extensions, so a refusal any of them throws is counted.

Functions (4)

hmacRoomLabelfunctionSource ↗

A stable room label that does not reveal the name: an HMAC of the name with your secret. The same name gives the same label across processes and restarts.

declare function hmacRoomLabel(secret: string | undefined): (name: string) => string;
```ts
const rooms = roomTelemetry({
  exporters: [metrics],
  roomLabel: hmacRoomLabel(process.env.ROOM_LABEL_SECRET),
});
```

requireBearerTokenfunctionSource ↗

An authorize check for a shared token, sent as Authorization: Bearer <token>. The comparison takes the same time whatever the token, and an empty secret throws at startup rather than letting every request in.

declare function requireBearerToken(secret: string | undefined): (request: onRequestPayload['request']) => boolean;
```ts
serveMetrics(metrics, { authorize: requireBearerToken(process.env.METRICS_TOKEN) });
```

serveMetricsfunctionSource ↗

An onRequest hook that serves Prometheus metrics from the Hocuspocus HTTP server itself, for a deployment without a separate web framework. Other requests fall through to the default handler.

declare function serveMetrics(registry: PrometheusTextSource, options?: ServeMetricsOptions): (payload: onRequestPayload) => Promise<void>;
```ts
new Server({
  ...config,
  onRequest: serveMetrics(metrics, {
    rooms: { telemetry: rooms, path: '/rooms' },
    authorize: requireBearerToken(process.env.METRICS_TOKEN),
  }),
});
```

withCollaborationTelemetryfunctionSource ↗

Add room telemetry to a Hocuspocus configuration.

It records rooms, connections, the people in each room (from awareness), room memory and size, load and store times, and why connections and messages were refused. Your hooks and your extensions run exactly as before, with the same arguments, return values, and this; their errors are rethrown. Extensions added later with server.configure are not observed. Requires @hocuspocus/server 4.6 or later.

declare function withCollaborationTelemetry<C extends Partial<ServerConfiguration>>(config: C, options: RoomTelemetryOptions): CollaborationTelemetry<C>;
```ts
import { Server } from '@hocuspocus/server';
import { withCollaborationTelemetry } from '@docx-editor.dev/telemetry/hocuspocus';
import { prometheusRegistry, metricsHandler } from '@docx-editor.dev/telemetry/prometheus';

const metrics = prometheusRegistry();
const { config, rooms } = withCollaborationTelemetry(
  { port: 1234, onAuthenticate, extensions: [new Database({ fetch, store })] },
  { exporters: [metrics] }
);
const server = new Server(config); // telemetry flushes when the server is destroyed

app.get('/metrics', metricsHandler(metrics));
```

Interfaces (11)

AwarenessLikeinterfaceSource ↗

The part of a y-protocols Awareness the telemetry reads.

interface AwarenessLike
MemberTypeSummary
getStates
off
on

CollaborationTelemetryinterfaceSource ↗

What [withCollaborationTelemetry](withCollaborationTelemetry) returns.

interface CollaborationTelemetry<C>
MemberTypeSummary
configCYour configuration with observed hooks. Pass it to `new Server(config)`.
roomsRoomTelemetryThe room telemetry, for `list()`, `sample()`, and `shutdown()`.

MetricsHandlerOptionsinterfaceSource ↗

Options for [metricsHandler](metricsHandler) and [fetchMetricsHandler](fetchMetricsHandler).

interface MetricsHandlerOptions
MemberTypeSummary
merge?readonly PrometheusTextSource[]More sources rendered into the same response, such as your `prom-client` registry: `merge: [register]`.

PrometheusTextSourceinterfaceSource ↗

Anything that renders Prometheus text, such as a prom-client Registry.

interface PrometheusTextSource
MemberTypeSummary
metrics

RoomHandleinterfaceSource ↗

One open room. Returned by [RoomTelemetry.open](RoomTelemetry.open).

interface RoomHandle
MemberTypeSummary
beginStart timing `operation` where `time` cannot wrap it; call the result once it settles.
closeStop tracking the room, for example when the server unloads it.
connected
disconnected
labelstringThe room's label, as [RoomTelemetryOptions.roomLabel](RoomTelemetryOptions.roomLabel) made it.
timeRun `operation` and record its duration and outcome. Rethrows its error.

RoomStatsinterfaceSource ↗

One room in [RoomTelemetry.list](RoomTelemetry.list).

interface RoomStats
MemberTypeSummary
ageSecondsnumber
blobBytesnumber | null
connectionsnumber
idleSecondsnumber
limitRationumber | nullThe largest share of any room limit in use, from 0 to 1.
nodesnumber | null
participantsnumberPeople present, from awareness states. A person with two tabs counts twice.
pendingBytesnumberBytes of updates applied since the room was last stored.
roomstring
stateBytesnumber | nullEncoded Yjs state at the last sample, or `null` before the first one.

RoomTelemetryinterfaceSource ↗

Room telemetry for one server process.

interface RoomTelemetry
MemberTypeSummary
beginStart timing an operation that has no room yet; call the result once it settles.
flush
listEvery open room, for an admin endpoint you protect.
migratedA room migration finished.
openStart tracking a loaded room. Pass the room's awareness to count people; without it the number of connections stands in.
refusedA message refused before it applied, such as `room-generation-changed`.
rejectedA connection refused at admission. `reason` can be a string or an error with `reason`.
sampleSample people, memory, and size now.
shutdownStop the timer, close every room, and shut the exporters down.
timeTime an operation that happens before the room is open, such as a load that fails.

RoomTelemetryOptionsinterfaceSource ↗

Options for [roomTelemetry](roomTelemetry).

interface RoomTelemetryOptions extends TelemetryPipelineOptions
MemberTypeSummary
measureState?booleanMeasure each room's encoded Yjs state. This encodes the room once per sample, and only when it changed since the last one. Defaults to `true`.
perRoomSeries?booleanAlso record each room's people and memory as gauges labeled `room`. Off by default: a label per room makes one series per room, and a large server has many rooms. A closed room's series is dropped. Only the default label or `hmacRoomLabel` is accepted, so no room name can become a label; any other `roomLabel` throws.
reasonOf?(error: unknown) => string | undefinedYour own reading of a refusal, for errors whose `reason` or `code` is free text. Return a common reason such as `unauthorized`, a collaboration failure code, or one of `reasons`; anything else is counted as `other`. Return undefined to use the default reading.
reasons?readonly string[]Refusal reasons to keep as metric labels. Compared without case and recorded in lowercase. Collaboration failure codes are always kept. Any other reason is counted as `other`, so a free-text error never becomes a time series.
roomLabel?(name: string) => stringHow a room appears in [RoomTelemetry.list](RoomTelemetry.list). Defaults to an index that is valid only inside this process, such as `room-3`. Use `(name) => name` to see real names, or [hmacRoomLabel](hmacRoomLabel) for a stable label that does not reveal the name.
sampleInterval?number | falseHow often to sample people, memory, and room size, in milliseconds. Defaults to 60 000. `false` turns the timer off; call `sample()` yourself, for example before each scrape.

ServeMetricsOptionsinterfaceSource ↗

Options for [serveMetrics](serveMetrics).

interface ServeMetricsOptions extends MetricsHandlerOptions
MemberTypeSummary
authorize?(request: onRequestPayload['request']) => booleanReturn false to answer 403. Use [requireBearerToken](requireBearerToken) for a shared scrape token. Applies to both paths.
next?(payload: onRequestPayload) => Promise<unknown>Your own `onRequest`, called for every request this hook does not answer.
path?stringThe path Prometheus scrapes. Defaults to `/metrics`.
rooms?{ readonly telemetry: RoomTelemetry; readonly path: string; }Also serve `rooms.list()` as JSON at this path. It lists every room, so `authorize` is required with it.
sampleBeforeScrape?RoomTelemetrySample this room telemetry before a scrape, for a server that sets `sampleInterval: false`. At most one sample is taken every 5 seconds, so several scrapers do not add extra samples.

TelemetryExporterinterfaceSource ↗

Where telemetry goes. Implement event, metric, or both.

An exporter never throws into the editor or the server: a failure is caught and reported once through onExporterError.

interface TelemetryExporter
```ts
const toMyLogger: TelemetryExporter = {
  name: 'my-logger',
  event: (event) => myLogger.warn(event.kind, { code: 'code' in event ? event.code : undefined }),
};
```
MemberTypeSummary
attachCalled once when a telemetry client or room server takes the exporter. An exporter that holds events, such as a batching one, reads `consented()` before it sends, so events queued before consent was withdrawn are discarded, not sent.
declareStart a series at zero before its first measurement. A scraper that sees a counter first at 1 cannot count that first increase, so the room server declares its known series when it starts. Optional: an exporter without it creates each series at its first measurement.
event
flushSend anything buffered. Called by `flush()` and when a page is hidden.
forgetDrop a series that no longer exists, such as a closed room's per-room gauge. Optional: an exporter without it keeps the series until it expires on its own.
metric
namestringShown in `onExporterError` reports.
shutdownFlush and release resources. Called by `shutdown()`.

TelemetryPipelineOptionsinterfaceSource ↗

Options shared by the editor client and the room server.

interface TelemetryPipelineOptions
MemberTypeSummary
exportersreadonly TelemetryExporter[]Where telemetry goes. Nothing is recorded or sent without at least one exporter.
metricLabels?readonly string[]Keys of your own attributes that may also label metrics, such as `tenant.tier` or `app.region`. Only for values with few distinct values: each value makes a new series. After [MAX_METRIC_LABEL_VALUES](MAX_METRIC_LABEL_VALUES) distinct values of one key, later values become `other`. Never list a user, document, or room id here; put those on events instead.
onExporterError?(error: unknown, exporter: string) => voidTold once per exporter when it throws. Defaults to one `console.warn`.
resource?AttributesValues added to every event, such as `{ 'service.name': 'contracts-app' }`. Metrics do not carry them: set resource attributes on your OpenTelemetry SDK or Prometheus target instead. You own these values: do not put a user, a document, or a room name here.

Type aliases (2)

RoomOperationtypeSource ↗

What a timed room operation is.

type RoomOperation = 'load' | 'store';

SettleOperationtypeSource ↗

Records a started operation's duration with its outcome. Later calls do nothing.

type SettleOperation = (outcome: 'success' | 'failure') => void;