Server credential
Create a scoped key in onboarding or settings. Never place it in browser or mobile code.
Canonical AI Telemetry V1
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.
Create a scoped key in onboarding or settings. Never place it in browser or mobile code.
Operation accepted/terminal, attempt started/terminal, and optional provider probes.
Prompts, media, identities, URLs, raw errors, stack traces, and credentials are rejected or removed.
Quickstart · raw HTTP
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.mjsCreate a workspace
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
| Your application | SignalOps boundary | Emit when |
|---|---|---|
| Durable job accepted | operation.accepted | Your system commits responsibility for the operation. |
| Provider call starts | attempt.started | A request is about to cross the provider seam. |
| Provider call ends | attempt.terminal | The exact attempt succeeds, fails, expires, or is cancelled. |
| Customer-visible job ends | operation.terminal | The overall operation reaches one final outcome. |
| Synthetic route check | provider.probe | A bounded health probe completes; optional and separate from user work. |
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