Help improve docs quality by sharing anonymous interaction telemetry (no PII or token data).

OpenTelemetry Visibility

Export decision spans and bounded metrics with deep links to exact evaluation records.

Entitlement checks are control-flow: they decide whether requests proceed, degrade, or get billed. When they misbehave — slow, degraded, or suddenly denying — you want to see it in the same dashboards and traces as the rest of your system, not in a separate silo. The packaged exporter registers instrumentation against your existing OpenTelemetry setup; it composes with any OTLP-speaking vendor and no-ops silently when no OTel SDK is registered.

Enable in one line

instrumentMonetizeKit() returns a DecisionObserver that emits spans and metrics through your process's registered tracer and meter providers:

import { MonetizeKit } from "@monetizekit/node";
import { instrumentMonetizeKit } from "@monetizekit/node/otel";

const apiKey = process.env.MONETIZEKIT_API_KEY;
if (!apiKey) throw new Error("MONETIZEKIT_API_KEY is required");

export const mk = new MonetizeKit({
  apiKey,
  observers: [instrumentMonetizeKit()],
});

Sampling is parent-based and never overridden: if the caller's trace is unsampled, the decision span is too. Observer failures are swallowed twice over (by the SDK and by the exporter itself) — a broken telemetry pipeline can never affect an entitlement decision.

What gets emitted

One span per decision — monetizekit.entitlement_check for single checks, monetizekit.batch_check for batch results — nested inside whatever span is active when the check runs. Names and attributes are stable public API: renames are treated as breaking changes and ship with migration notes.

Span attributeMeaning
monetizekit.customer_idMonetizeKit customer id the decision was made for.
monetizekit.feature_keyFeature key that was checked.
monetizekit.decision“allowed” or “denied” — the product decision.
monetizekit.reason_codeStable machine-readable cause (granted, limit_reached, not_in_plan, …).
monetizekit.cache_hitTrue when served from the SDK's local cache. Cached decisions are real spans too — flagged, not hidden.
monetizekit.degradedTrue when the API was unreachable and the SDK's degradation policy answered.
monetizekit.evaluation_idId of the evaluation-log record behind the decision (when the API supplied one).
monetizekit.inspector_urlReady-made dashboard deep link to that exact evaluation record.
monetizekit.errorThe transport error message, on API failures only.

Metrics: bounded by design

Three instruments cover the operational questions. Metric attributes carry decision, cache, degradation, and feature-key dimensions — and deliberately never customer or evaluation ids, so time-series cardinality (and your monitoring bill) cannot grow with your account count:

InstrumentKindMeaning
monetizekit.check.durationHistogram (ms)End-to-end check duration — cache hits and API round trips, distinguishable by attribute.
monetizekit.checksCounterDecision volume, dimensioned by decision, cache_hit, degraded, and feature_key.
monetizekit.degraded_checksCounterDecisions served by the fail-open/fail-closed fallback. Alert when this is nonzero.

Customer-level detail belongs on spans and in the analytics stream, not in metrics. Traces and metrics are independently enableable via instrumentMonetizeKit({ traces, metrics }).

From a span to the exact evaluation

Every check response carries the id of its evaluation-log record as evaluationId, and the exporter renders it as a ready-made deep link: monetizekit.inspector_url points at /observability/inspector/{evaluationId} in the dashboard, which resolves that one record directly — plan version, resolution reason, usage context, evaluation timing. No searching. A trace showing a denial becomes the evaluation record explaining it, one click apart.

ToolRendering the link
DatadogFacet the monetizekit.inspector_url span attribute; URL-valued facets render as clickable links in the trace side panel.
Grafana (Tempo)Add a data link on the span attribute using ${__data.fields["monetizekit.inspector_url"]} in the trace panel's Data links settings.
HoneycombURL-shaped column values are clickable in trace and query results out of the box — no configuration needed.
SentrySpan attributes render in the trace detail view; URL values are shown as links in the span attributes table.

Alerts worth paging on

Express these in your metrics backend of choice — the instrument and attribute names are the ones emitted above:

AlertExpressionWhat it means
Degraded decisionsrate of monetizekit.degraded_checks > 0The SDK cannot reach the API and is serving fallback decisions — your gates are running on guesses.
Fallback sharedecisions with monetizekit.reason_code in (sdk_fail_open, sdk_fail_closed) as a % of all decisionsHow much of your traffic was decided by the fail-open/fail-closed policy, not the API.
Gate latencyp95 of monetizekit.check.duration where monetizekit.cache_hit = false, per feature_keyUncached checks sit on your hot path — a p95 regression here is user-visible request latency.
Denial spikesudden change in monetizekit.checks where monetizekit.decision = denied, per feature_keyA plan publish or catalog change may have unintentionally revoked access — correlate with your deploy/config timeline.

Manual pattern: span events instead of spans

If you'd rather attach decisions to your existing request spans than create a child span per check, implement DecisionObserver directly — the packaged exporter and this pattern use the same extension point and the same attribute names:

import { trace } from "@opentelemetry/api";
import { MonetizeKit, type DecisionObserver } from "@monetizekit/node";

const spanEventObserver: DecisionObserver = {
  onDecision(event) {
    trace.getActiveSpan()?.addEvent("monetizekit.decision", {
      "monetizekit.feature_key": event.featureKey ?? "",
      "monetizekit.decision": event.allowed ? "allowed" : "denied",
      "monetizekit.reason_code": event.reasonCode ?? "",
      "monetizekit.cache_hit": event.cached,
      "monetizekit.degraded": event.degraded,
      "monetizekit.latency_ms": event.latencyMs,
    });
  },
};

export const mk = new MonetizeKit({
  apiKey: process.env.MONETIZEKIT_API_KEY,
  observers: [spanEventObserver],
});

FAQ

Do I need a specific OTel vendor or collector?

No. The exporter depends only on @opentelemetry/api, which delegates to whatever SDK your process registered — any OTLP-speaking backend works. Without a registered SDK the calls are no-ops with no warnings, so the observer is safe to ship before your telemetry stack is wired up.

Will this flood my tracing bill with cache-hit spans?

Cache-served decisions are emitted as spans flagged monetizekit.cache_hit=true, because a decision your code acted on that is invisible in the trace is a debugging trap. Sampling is parent-based: decision spans are only recorded inside traces you already chose to sample. If you still want fewer spans, use the manual span-event pattern, or disable traces and keep metrics.

Can I use both this and the PostHog destination?

Yes — they answer different questions from different vantage points. OTel telemetry is generated in your process and tells you whether gating is healthy (latency, degradation, error rates); the PostHog destination streams denials from the platform side and tells you whether packaging is working (who hits limits, what converts). Neither depends on the other.

What do degraded spans look like during an outage?

Span status ERROR (the API call failed), with monetizekit.degraded=true, monetizekit.error carrying the transport message, and monetizekit.reason_code set to sdk_fail_open or sdk_fail_closed (or a stale-cache reason when the SDK could serve real, older data). One alert on monetizekit.degraded_checks distinguishes “the API is down” from “customers are hitting limits” — which is the point.

Node SDK Integration

The DecisionObserver extension point this guide builds on.

Open guide

Audit and Trace

Request-id propagation and the audit log, end to end.

Open guide

PostHog Packaging Analytics

The product-analytics twin of this guide's operational signals.

Open guide

Was this page helpful?