SignalOpsPublic beta

Canonical AI Telemetry V1

Instrument once. Keep every client adapter replaceable.

SignalOps consumes five privacy-safe lifecycle boundaries. Your product retains its provider-management architecture; the adapter maps local facts into a stable, vendor-neutral contract.

Server credential

Create a scoped key in onboarding or settings. Never place it in browser or mobile code.

Five boundaries

Operation accepted/terminal, attempt started/terminal, and optional provider probes.

Privacy gate

Prompts, media, identities, URLs, raw errors, stack traces, and credentials are rejected or removed.

Quickstart · raw HTTP

Send a useful operation without installing a package.

Save the script as quickstart.mjs, provide your one-time credential through the environment, and run it on Node 20+.

SIGNALOPS_INGEST_CREDENTIAL="sop_live_…" node quickstart.mjs
Create a workspace
quickstart.mjs
const endpoint = "https://signalops.cc/v1/events";
const credential = process.env.SIGNALOPS_INGEST_CREDENTIAL;
if (!credential) throw new Error("SIGNALOPS_INGEST_CREDENTIAL is required");

const operationId = `quickstart_${crypto.randomUUID()}`;
const attemptId = `attempt_${crypto.randomUUID()}`;
const source = "urn:quickstart:generation-worker";
const resource = {
  environment: "production",
  service: "generation-worker",
};
const operation = {
  id: operationId,
  kind: "text_generation",
  logicalModelKey: "quickstart-model",
};
const route = {
  providerKey: "primary",
  providerVendor: "example",
  modelKey: "quickstart-model",
  providerModelKey: "example-model-v1",
};
const envelope = (boundary, type, data) => ({
  specversion: "1.0",
  id: `evt_${operationId}_${boundary}`,
  source,
  type,
  subject: `operation/${operationId}`,
  time: new Date().toISOString(),
  datacontenttype: "application/json",
  dataschema: "https://signalops.cc/schemas/ai-telemetry/v1",
  data,
});

const events = [
  envelope("accepted", "com.signalops.ai.operation.accepted.v1", {
    operation,
    resource,
  }),
  envelope("attempt_started", "com.signalops.ai.attempt.started.v1", {
    operation,
    attempt: { id: attemptId, number: 1 },
    route,
    resource,
  }),
  envelope("attempt_terminal", "com.signalops.ai.attempt.terminal.v1", {
    operation,
    attempt: { id: attemptId, number: 1 },
    route,
    outcome: { status: "succeeded" },
    metrics: { durationMs: 420, outputUnits: 1 },
    cost: { amount: "0.001", currency: "USD", source: "provider_reported" },
    resource,
  }),
  envelope("terminal", "com.signalops.ai.operation.terminal.v1", {
    operation,
    outcome: { status: "succeeded" },
    metrics: { totalDurationMs: 480, attemptCount: 1 },
    resource,
  }),
];

const response = await fetch(endpoint, {
  method: "POST",
  headers: {
    authorization: `Bearer ${credential}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({ events }),
});
const result = await response.json();
if (!response.ok || !result.ok) throw new Error(JSON.stringify(result));
console.log("SignalOps accepted", result.receipt.storedEvents, "events for", operationId);

Lifecycle model

Client facts map to canonical facts.

Your applicationSignalOps boundaryEmit when
Durable job acceptedoperation.acceptedYour system commits responsibility for the operation.
Provider call startsattempt.startedA request is about to cross the provider seam.
Provider call endsattempt.terminalThe exact attempt succeeds, fails, expires, or is cancelled.
Customer-visible job endsoperation.terminalThe overall operation reaches one final outcome.
Synthetic route checkprovider.probeA bounded health probe completes; optional and separate from user work.

Production delivery checklist

  • Emit after durable state transitions, not before database commit.
  • Derive stable event IDs so delivery retries are idempotent.
  • Persist dead letters or use your existing durable outbox.
  • Keep provider request ID, operation ID, and model keys opaque.
  • Use normalized failure category, responsibility, code, and retryability.

Universal adapter boundary

  • Core contract has no Nuxt, Next, Supabase, queue, or provider dependency.
  • Each client owns a thin mapper at its actual lifecycle seams.
  • Provider connection keys remain opaque; SignalOps never manages customer credentials.
  • Conformance scenarios prove success, retry, fallback, failure, privacy, and replay.
  • The Node producer in this repository is release-ready; registry publication is tracked separately.

Need a client-specific adapter?

Start from the generic contract and conformance suite. We can review lifecycle seams for Nuxt, Next.js, queue workers, or a custom runtime without forcing an application rewrite.

Discuss an adapter