@clue-ai/backend-sdk
v1.0.0
Published
Clue Backend SDK (Node.js) — OTel-based observation-source event capture for Express, NestJS, MCP, LangChain, Mastra, and AI providers
Downloads
61
Readme
@clue-ai/backend-sdk
Clue Node.js / Express backend SDK. Events flow through a
NodeTracerProvider + BatchSpanProcessor +
ObservationSourceEventExporter to
/api/v1/ingest/backend.
Minimal integration
import clue from "@clue-ai/backend-sdk";
clue.init({
endpoint: process.env.CLUE_INGEST_ENDPOINT!,
projectKey: process.env.CLUE_PROJECT_KEY!,
apiKey: process.env.CLUE_API_KEY!,
// Literal service label from Clue setup output. Do not expose this to the browser.
serviceKey: "backend-api",
});
clue.identify(user.id, { name: user.name, email: user.email });
clue.group("organization", organization.id, {
name: organization.name,
plan: organization.plan,
});
clue.track("order_placed", { product_name: "shirt", amount: 100 });
// Call on logout/session reset success paths.
clue.reset();
await clue.flush();Do not pass environment; Clue derives dev/prod from the projectKey prefix.
If the customer product calls its company concept account, workspace, tenant, or
team, pass that stable company id to clue.group("organization", ...) for MVP.
clue.track custom event names must match
^[A-Za-z0-9_.:-]{1,128}$. Invalid names are ignored and do not emit a
canonical event.
Privacy and PII handling
1. Hard-deny: PII / secrets are stripped before transport
The SDK strips a built-in set of property keys before the value is
set as an OTel span attribute (i.e. before it leaves the customer
process). Caller-supplied deniedKeys are added on top of the
hardcoded ALWAYS_DENIED_KEYS — they cannot remove a default-denied
key.
Hard-deny categories (case- and separator-insensitive — userEmail,
user-email, USER_EMAIL, email_address all match):
- Auth credentials:
authorization,cookie,set-cookie,password,passwd,secret,token,access_token,refresh_token,session,session_token,api_key,apikey,private_key - PII categories:
email,phone,credit_card,ssn
This list is a strict superset of the server-side ingest hard-deny
(@clue/shared INGEST_HARD_DENY_KEYS), enforced by a CI parity test
in test/sanitize.test.ts.
Nested objects in clue.track properties are recursively sanitized —
clue.track("x", { user: { email: "[email protected]", name: "Alice" } }) emits
the name field but never transmits email.
2. Default-masked: configure server allowlist for analysis projection
Properties that pass the hard-deny gate are still masked by default
on the server side. To make a property appear in Clue's analysis UI,
the property key must be listed on the resolved project service's
observationAnalysisPropertyAllowlist. Without this, the property lands
in raw_only_keys (visible in raw archive only) and not in
analysisProperties.projected.
Configure the allowlist via the Clue admin UI or the project service allowlist API after taking a privacy review of which property names you want to project for analytics.
3. Identity-event email pass-through is intentional
clue.identify(userId, { email: "..." }) populates a structured
user_profile.email field on the canonical identity event. This is a
deliberate, opt-in identity contract — the SDK does NOT strip the
email trait on identity-event paths because the canonical identity
event would be meaningless without it. The server-side worker still
applies the hard-deny gate after backend ingest if the field path
matches.
Source hints (auto-emitted on every event)
Every canonical event ships with 8 source hint fields so the Clue worker can derive customer-value concepts (observation, behavior unit, operation, value achievement) without having to redo backend-side context inspection. The SDK fills every hint automatically — no host-side API call is required.
| Hint | What it captures |
| --- | --- |
| subject_hint | anonymous / user / organization identifiers from the request context |
| actor_hint | who initiated the event — defaults to system / backend for server-side work; overridable via hintOptions |
| target_object_canonical_hint | which object the operation acted on — auto-derived from repositoryName + mutationKey for repository mutations |
| semantic_hint | which action_key / operation_key the builder declared (= commandKey, mutationKind, transitionKey) |
| identity_link_hint | only on identity lifecycle events — anonymous_to_user / user_to_organization / logout |
| context_snapshot_hint | plan / role / feature flags / experiment variant pulled from the backend context |
| correlation | trace / request span / interaction / session identifiers for cross-event joining |
| privacy_summary | masking decisions applied to the event — level + masked field count + rule set version |
All fields are optional on ingest; the worker treats missing hints as "unknown" rather than rejecting the event.
Internal-flow builders (= domain command / mutation / state transition)
For service-internal flows that don't map cleanly to a single HTTP
request, use the dedicated builders. Each one auto-fills
semantic_hint and target_object_canonical_hint from the structured
keys you already pass:
import {
buildDomainCommandEvent,
buildRepositoryMutationEvent,
buildStateTransitionEvent,
type BaseEventInput,
} from "@clue-ai/backend-sdk";
const baseInput: BaseEventInput = {
sdkCollectionMode: "standard",
userId: "user_42",
organizationId: "org_3",
// …trace / request span / interaction ids
};
const command = buildDomainCommandEvent({
...baseInput,
commandKey: "checkout.complete",
});
const mutation = buildRepositoryMutationEvent({
...baseInput,
repositoryName: "order_repository",
mutationKind: "update",
mutationKey: "order.cancel",
maskedEntityId: "ord_99_masked",
});
const transition = buildStateTransitionEvent({
...baseInput,
transitionKey: "order.paid_to_shipped",
});buildDomainCommandEvent→semantic_hint.action_key = commandKey,operation_key = operationKey ?? commandKey.buildRepositoryMutationEvent→target_object_canonical_hintbuilt fromrepositoryName+maskedEntityId;semantic_hint.action_key = mutationKind.buildStateTransitionEvent→semantic_hint.action_key = transitionKey;confidencedefaults tohigh.
Overriding hints via BaseEventHintOptions
Every public builder accepts an optional third argument
(BaseEventHintOptions) so application code can pin a richer hint
than the auto-derived default — for example, when the host knows the
actor is a human admin rather than the backend itself:
import {
buildRepositoryMutationEvent,
type BaseEventHintOptions,
} from "@clue-ai/backend-sdk";
const hintOptions: BaseEventHintOptions = {
actorType: "human_user",
actorProvider: null,
hasInteractionId: true,
targetObjectCanonicalHint: {
type: "report",
id: "rep_77",
name: "Quarterly Report",
key: "reports.quarterly",
stable_key: "reports.quarterly",
stable_key_quality: "spec",
},
targetObjectCanonicalHintSource: "backend",
};
buildRepositoryMutationEvent(
{
...baseInput,
repositoryName: "report_repository",
mutationKind: "update",
mutationKey: "reports.quarterly.update",
maskedEntityId: "rep_77_masked",
},
/* properties */ {},
/* metrics */ {},
hintOptions,
);BaseEventHintOptions accepts overrides for actor type / provider,
target object hint, semantic hint, identity link hint, context (path /
URL), and privacy summary. Each is merged onto the builder default —
you only need to provide the fields you want to override.
Build / test
pnpm install
pnpm build
pnpm test # unit + parity tests
pnpm test:cov # with coverageLive-verify (opt-in, requires apps/api + apps/worker running):
CLUE_LIVE_VERIFY=1 \
CLUE_LIVE_PROJECT_KEY=pk_dev_xxx \
CLUE_LIVE_API_KEY=cluesk_dev_xxx \
pnpm exec vitest run test/canonical-endpoint-live.test.ts