USAGE GUIDE

Connect your Node.js app.
See its first evidence.

Create a workspace, save its key, add observation code, and check the first execution in your dashboard.

BEFORE SETUP

Start with one staging delivery.

Try a small path you can inspect and remove before adding observations to more traffic.

You need Node.js 22.12 or later with ESM, an existing PostgreSQL transaction/outbox and BullMQ delivery path, and permission to change its server code and store observation context. The SDK does not create the database schema or instrument your application automatically.

  1. Choose a controlled test.

    Use one staging path and a synthetic operation with an expected business result. Limit the observation calls to that path. You can first run the local example without an account; it uses PostgreSQL 16, Redis 7, and its own synthetic data.

  2. Confirm the first three observations.

    The helper below reports an HTTP request, a database commit and an outbox write when the transaction succeeds: up to three collection attempts for that invocation. A connection check is another attempt. Review the beta limits; attempts are not a guaranteed number of complete deliveries.

  3. Extend only to boundaries you can observe.

    Carry the same stored context into the relay and worker. Receiver instrumentation is optional: if you cannot observe its committed effect, the final outcome stays unconfirmed. Compare the timeline with your test’s known business result before expanding the trial.

What changes in each process and in storage?

Scroll the table sideways to see all three columns.

WhereYour code changeDurable context
API / transactionCreate the opaque observation identity; emit commit and outbox success only after the real commit acknowledgement.Save executionId, operationId, tenantRef and the returned evidenceContext together with the operation/outbox in your transaction. Adapt your schema or existing metadata storage.
Outbox relayRestore trusted context; observe the queue acknowledgement.Reuse that record. The job may carry an opaque reference so the worker can load it; never replace the identity downstream.
BullMQ workerRestore context; observe the worker result and webhook response separately.Load the same trusted database/job record and validate its identity with contextFromTrustedRecord.
Receiver, if observableReport an external effect only after its actual business commit is confirmed.Resolve trusted context through your integration. An arbitrary incoming webhook body cannot authorize observation.

Create one reporter per participating process. Keep the returned disabled/interrupted decision and leave legacy records without context unobserved. Observation fields and the ingest key have separate purposes; never save the key in an outbox or job.

See how to stop collection and remove the integration before starting.

01 / GET STARTED

Create a workspace and save its key.

Join the free beta with GitHub, create a workspace, and connect your Node.js application. Start with one HTTP request → PostgreSQL outbox → BullMQ worker → webhook path.

  1. Review the terms and log in with GitHub.

    Read the versioned evaluation terms and privacy notice before choosing Continue with GitHub. Terms acceptance is a separate, explicit choice.

  2. Create your workspace and save its ingest key.

    Open Connect, confirm that you can authorize collection for the application, and create a workspace. The key is shown once. Save it in your server’s secret environment, then confirm that you saved it before opening the installation guide. Your login and workspace key have separate purposes: one lets you read evidence, the other lets your application send it.

  3. Connect the application and read its first evidence.

    Follow the installation guide below, check the connection, and run your application’s normal delivery path. Open the dashboard, choose Refresh, and select an execution to read its timeline. With multiple workspaces, choose the workspace you connected.

02 / ADD OBSERVATION CODE

Add the SDK to your Node.js app.

You need Node.js 22.12 or later, ESM, and access to your application’s server code. Installing the package alone does not create evidence.

1. Install the SDK in your application.

SDK install command
npm install https://reliability.nestarc.dev/downloads/nestarc-evidence-0.3.0.tgz

Use .mjs files or an ESM project with "type": "module". The version 0.3.0 package includes the full guide at node_modules/@nestarc/evidence/integration-guide.md.

Review the SDK source, checksum and release notes before installing it in your server.

Inspect SDK 0.3.0: source, checksum and changes

Nestarc distributes @nestarc/evidence directly as a versioned archive. You can inspect the JavaScript below before installing it. These review materials do not change the 0.3.0 package.

Verify the archive before installing.

Download the SDK 0.3.0 archive and calculate its SHA-256 locally. Compare the entire value with the expected checksum below.

Download and checksum commands
curl --fail --show-error --output nestarc-evidence-0.3.0.tgz https://reliability.nestarc.dev/downloads/nestarc-evidence-0.3.0.tgz
shasum -a 256 nestarc-evidence-0.3.0.tgz

Expected SHA-256: 4ffb06af8f2d5a4457238695e0010fd3fe6c79139579695b16e094e5166485c5
Archive size: 30,459 bytes.

If it matches, install the file you checked with npm install ./nestarc-evidence-0.3.0.tgz. A mismatch is a reason to stop and check the download.

What changed in 0.3.0?

  • Version 0.3.0 adds a bounded in-memory waiting queue: one active send and up to 16 waiting observations per reporter by default.
  • Each observation keeps its event ID and original occurrence time. Its total lifetime, including queueing and sending, is capped at 30 seconds.
  • A send remains bounded while its response body is cancelled. Timeout, network and response-cleanup losses have separate aggregate counters.
  • Failed sends are not automatically retried. Evidence export does not determine your business result.

Read the code shipped in this archive.

The seven JavaScript runtime modules shipped inside this exact SDK archive. Type declarations, integration guidance and a local example are also included in the archive. The code is shown as text and is not executed in your browser.

dist/index.js
export * from "./classifier.js";
export * from "./schema.js";
export * from "./delivery-context.js";
dist/schema.js
import { z } from "zod";
export const EVIDENCE_SCHEMA_VERSION = "nestarc.evidence.v1";
export const sourceEvidenceStageSchema = z.enum([
    "http.request",
    "database.commit",
    "outbox.write",
    "queue.enqueue",
    "worker.result",
    "webhook.request",
    "external.effect",
    "connector",
]);
export const sourceEvidenceStatusSchema = z.enum([
    "started",
    "succeeded",
    "accepted",
    "failed",
    "interrupted",
]);
export const evidenceErrorCodeSchema = z.enum([
    "WORKER_FAILED",
    "WEBHOOK_TIMEOUT",
    "CONNECTOR_INTERRUPTED",
    "INTERNAL_ERROR",
]);
export const opaqueReferenceSchema = z
    .string()
    .regex(/^[A-Za-z0-9._:-]{1,128}$/, "Must be an opaque 1-128 character reference");
export const utcIsoTimestampSchema = z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, "Must be an ISO 8601 timestamp in UTC")
    .datetime({ offset: false });
function normalizeUtcIsoTimestamp(value) {
    const fractionStart = value.indexOf(".");
    if (fractionStart === -1) {
        return `${value.slice(0, -1)}.000000000Z`;
    }
    const fraction = value.slice(fractionStart + 1, -1).padEnd(9, "0");
    return `${value.slice(0, fractionStart)}.${fraction}Z`;
}
export function compareUtcIsoTimestamps(left, right) {
    const normalizedLeft = normalizeUtcIsoTimestamp(left);
    const normalizedRight = normalizeUtcIsoTimestamp(right);
    if (normalizedLeft === normalizedRight)
        return 0;
    return normalizedLeft < normalizedRight ? -1 : 1;
}
const sourceEvidenceEventObjectSchema = z
    .object({
    schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
    eventId: z.string().uuid(),
    executionId: opaqueReferenceSchema,
    operationId: opaqueReferenceSchema,
    tenantRef: opaqueReferenceSchema.optional(),
    stage: sourceEvidenceStageSchema,
    status: sourceEvidenceStatusSchema,
    occurredAt: utcIsoTimestampSchema,
    attempt: z.number().int().min(1).max(100).optional(),
    durationMs: z.number().int().min(0).max(86_400_000).optional(),
    errorCode: evidenceErrorCodeSchema.optional(),
})
    .strict();
function hasCompatibleErrorContext(event) {
    switch (event.errorCode) {
        case undefined:
            return true;
        case "WORKER_FAILED":
            return event.stage === "worker.result" && event.status === "failed";
        case "WEBHOOK_TIMEOUT":
            return event.stage === "webhook.request" && event.status === "failed";
        case "CONNECTOR_INTERRUPTED":
            return (event.stage === "connector" &&
                (event.status === "failed" || event.status === "interrupted"));
        case "INTERNAL_ERROR":
            return event.status === "failed";
    }
}
const compatibleErrorContext = {
    message: "Error code must match its source Evidence stage and status",
    path: ["errorCode"],
};
export const evidenceEventSchema = sourceEvidenceEventObjectSchema.refine(hasCompatibleErrorContext, compatibleErrorContext);
export const receivedEvidenceEventSchema = sourceEvidenceEventObjectSchema
    .extend({
    receivedAt: utcIsoTimestampSchema.optional(),
})
    .strict()
    .refine(hasCompatibleErrorContext, compatibleErrorContext);
export const executionStateSchema = z.enum([
    "confirmed-complete",
    "confirmed-failure",
    "outcome-unknown",
    "evidence-interrupted",
]);
export const classificationReasonSchema = z.enum([
    "external-effect-confirmed",
    "failure-reported",
    "webhook-outcome-unconfirmed",
    "evidence-stream-interrupted",
    "execution-in-progress",
]);
export const CLASSIFICATION_STATE_BY_REASON = {
    "external-effect-confirmed": "confirmed-complete",
    "failure-reported": "confirmed-failure",
    "webhook-outcome-unconfirmed": "outcome-unknown",
    "evidence-stream-interrupted": "evidence-interrupted",
    "execution-in-progress": "outcome-unknown",
};
export const timelineToneSchema = z.enum([
    "neutral",
    "positive",
    "warning",
    "negative",
]);
export const timelineEntrySchema = z
    .object({
    sequence: z.number().int().positive(),
    eventId: z.string().uuid(),
    stage: sourceEvidenceStageSchema,
    status: sourceEvidenceStatusSchema,
    occurredAt: utcIsoTimestampSchema,
    receivedAt: utcIsoTimestampSchema.optional(),
    attempt: z.number().int().min(1).max(100).optional(),
    durationMs: z.number().int().min(0).max(86_400_000).optional(),
    errorCode: evidenceErrorCodeSchema.optional(),
    title: z.string().min(1),
    tone: timelineToneSchema,
})
    .strict();
export const executionSummarySchema = z
    .object({
    executionId: opaqueReferenceSchema,
    operationId: opaqueReferenceSchema,
    tenantRef: opaqueReferenceSchema.optional(),
    state: executionStateSchema,
    reason: classificationReasonSchema,
    firstOccurredAt: utcIsoTimestampSchema,
    lastOccurredAt: utcIsoTimestampSchema,
    lastReceivedAt: utcIsoTimestampSchema.optional(),
    eventCount: z.number().int().positive(),
    retention: z.object({ windowDays: z.number().int().positive(), expiredHistory: z.boolean(), nextExpiryAt: utcIsoTimestampSchema.optional(), evaluatedAt: utcIsoTimestampSchema.optional() }).strict().optional(),
})
    .strict()
    .superRefine((execution, context) => {
    const expectedState = CLASSIFICATION_STATE_BY_REASON[execution.reason];
    if (execution.state !== expectedState) {
        context.addIssue({
            code: "custom",
            path: ["reason"],
            message: `Classification reason ${execution.reason} requires state ${expectedState}`,
        });
    }
});
export const classifiedExecutionSchema = executionSummarySchema
    .extend({
    timeline: z.array(timelineEntrySchema).min(1),
})
    .strict();
// A storage relationship problem is not an execution state or source Evidence.
export const executionDataIssueSchema = z
    .object({
    executionId: opaqueReferenceSchema,
    code: z.literal("EXECUTION_DATA_CONFLICT"),
})
    .strict();
export const listExecutionsResponseSchema = z
    .object({
    executions: z.array(executionSummarySchema),
    dataIssues: z.array(executionDataIssueSchema),
})
    .strict()
    .superRefine(({ executions, dataIssues }, context) => {
    const ids = [...executions, ...dataIssues].map(({ executionId }) => executionId);
    if (ids.length > 50 || new Set(ids).size !== ids.length) {
        context.addIssue({ code: "custom", message: "Recent results must contain at most 50 distinct executions" });
    }
});
export const ingestEvidenceResponseSchema = z
    .object({
    accepted: z.literal(true),
    duplicate: z.boolean(),
    eventId: z.string().uuid(),
    receivedAt: utcIsoTimestampSchema,
})
    .strict();
export const healthResponseSchema = z
    .object({
    status: z.literal("ok"),
})
    .strict();
export const apiErrorCodeSchema = z.enum([
    "UNAUTHORIZED",
    "INVALID_WORKSPACE_ID",
    "FORBIDDEN",
    "INVALID_JSON",
    "BODY_TOO_LARGE",
    "BODY_READ_TIMEOUT",
    "RATE_LIMITED",
    "LIMIT_UNAVAILABLE",
    "POLICY_ACCEPTANCE_REQUIRED",
    "POLICY_UNAVAILABLE",
    "INVALID_EVIDENCE",
    "INVALID_EXECUTION_ID",
    "EVENT_ID_CONFLICT",
    "EXECUTION_IDENTITY_CONFLICT",
    "EXECUTION_DATA_CONFLICT",
    "EXECUTION_NOT_FOUND",
    "CONFIGURATION_ERROR",
    "INTERNAL_ERROR",
]);
export const apiErrorResponseSchema = z
    .object({
    error: z
        .object({
        code: apiErrorCodeSchema,
        message: z.string().min(1),
    })
        .strict(),
})
    .strict();
dist/classifier.js
import { compareUtcIsoTimestamps } from "./schema.js";
const SOURCE_STAGE_TITLES = {
    "http.request": "HTTP request",
    "database.commit": "Database commit",
    "outbox.write": "Outbox write",
    "queue.enqueue": "Queue enqueue",
    "worker.result": "Worker result",
    "webhook.request": "Webhook request",
    "external.effect": "External effect",
    connector: "Evidence connector",
};
export class EvidenceInvariantError extends Error {
    constructor(message) {
        super(message);
        this.name = "EvidenceInvariantError";
    }
}
function compareEvents(left, right) {
    const occurredDifference = compareUtcIsoTimestamps(left.occurredAt, right.occurredAt);
    if (occurredDifference !== 0) {
        return occurredDifference;
    }
    return left.eventId.localeCompare(right.eventId);
}
function getLastReceivedAt(events) {
    let latest;
    for (const event of events) {
        if (event.receivedAt === undefined) {
            continue;
        }
        if (latest === undefined) {
            latest = event.receivedAt;
            continue;
        }
        const difference = compareUtcIsoTimestamps(event.receivedAt, latest);
        if (difference > 0 || (difference === 0 && event.receivedAt > latest)) {
            latest = event.receivedAt;
        }
    }
    return latest;
}
function timelineToneForSourceEvent(event) {
    if ((event.stage === "webhook.request" && event.errorCode === "WEBHOOK_TIMEOUT") ||
        (event.stage === "connector" &&
            (event.status === "failed" ||
                event.status === "interrupted" ||
                event.errorCode === "CONNECTOR_INTERRUPTED")) ||
        event.status === "interrupted") {
        return "warning";
    }
    if (event.status === "failed") {
        return "negative";
    }
    if (event.status === "succeeded" || event.status === "accepted") {
        return "positive";
    }
    return "neutral";
}
function toTimelineEntry(event, index) {
    return {
        sequence: index + 1,
        eventId: event.eventId,
        stage: event.stage,
        status: event.status,
        occurredAt: event.occurredAt,
        ...(event.receivedAt === undefined ? {} : { receivedAt: event.receivedAt }),
        ...(event.attempt === undefined ? {} : { attempt: event.attempt }),
        ...(event.durationMs === undefined ? {} : { durationMs: event.durationMs }),
        ...(event.errorCode === undefined ? {} : { errorCode: event.errorCode }),
        title: SOURCE_STAGE_TITLES[event.stage],
        tone: timelineToneForSourceEvent(event),
    };
}
function lastIndexMatching(events, predicate) {
    for (let index = events.length - 1; index >= 0; index -= 1) {
        const event = events[index];
        if (event !== undefined && predicate(event)) {
            return index;
        }
    }
    return -1;
}
function determineClassification(events) {
    const reversed = [...events].reverse();
    // LIFE-08: a later start/acceptance/interruption does not erase an explicit
    // external outcome report. Preserve source-time/event-ID order between
    // terminal reports; neither attempt numbers nor receipt times add authority.
    const externalEffect = reversed.find((event) => event.stage === "external.effect" &&
        (event.status === "succeeded" || event.status === "failed"));
    if (externalEffect?.status === "succeeded") {
        return {
            state: "confirmed-complete",
            reason: "external-effect-confirmed",
        };
    }
    if (externalEffect?.status === "failed") {
        return {
            state: "confirmed-failure",
            reason: "failure-reported",
        };
    }
    const confirmedFailureIndex = lastIndexMatching(events, (event) => event.status === "failed" &&
        event.stage !== "connector" &&
        !(event.stage === "webhook.request" && event.errorCode === "WEBHOOK_TIMEOUT"));
    const unconfirmedWebhookIndex = lastIndexMatching(events, (event) => event.stage === "webhook.request" &&
        (event.status === "accepted" ||
            event.status === "succeeded" ||
            event.errorCode === "WEBHOOK_TIMEOUT"));
    if (unconfirmedWebhookIndex > confirmedFailureIndex) {
        return {
            state: "outcome-unknown",
            reason: "webhook-outcome-unconfirmed",
        };
    }
    if (confirmedFailureIndex >= 0) {
        return {
            state: "confirmed-failure",
            reason: "failure-reported",
        };
    }
    const evidenceInterrupted = reversed.some((event) => event.stage === "connector" &&
        (event.status === "failed" ||
            event.status === "interrupted" ||
            event.errorCode === "CONNECTOR_INTERRUPTED"));
    if (evidenceInterrupted) {
        return {
            state: "evidence-interrupted",
            reason: "evidence-stream-interrupted",
        };
    }
    if (unconfirmedWebhookIndex >= 0) {
        return {
            state: "outcome-unknown",
            reason: "webhook-outcome-unconfirmed",
        };
    }
    return {
        state: "outcome-unknown",
        reason: "execution-in-progress",
    };
}
function assertConsistentExecution(events) {
    const first = events[0];
    if (first === undefined) {
        throw new EvidenceInvariantError("At least one evidence event is required");
    }
    const tenantRefs = new Set(events.flatMap((event) => (event.tenantRef === undefined ? [] : [event.tenantRef])));
    if (events.some((event) => event.executionId !== first.executionId || event.operationId !== first.operationId) ||
        tenantRefs.size > 1) {
        throw new EvidenceInvariantError("Evidence events must share an executionId, operationId, and tenantRef");
    }
}
export function classifyExecution(events) {
    assertConsistentExecution(events);
    const ordered = [...events].sort(compareEvents);
    const first = ordered[0];
    const last = ordered.at(-1);
    if (first === undefined || last === undefined) {
        throw new EvidenceInvariantError("At least one evidence event is required");
    }
    const classification = determineClassification(ordered);
    const tenantRef = ordered.find((event) => event.tenantRef !== undefined)?.tenantRef;
    const lastReceivedAt = getLastReceivedAt(ordered);
    return {
        executionId: first.executionId,
        operationId: first.operationId,
        ...(tenantRef === undefined ? {} : { tenantRef }),
        ...classification,
        firstOccurredAt: first.occurredAt,
        lastOccurredAt: last.occurredAt,
        ...(lastReceivedAt === undefined ? {} : { lastReceivedAt }),
        eventCount: ordered.length,
        timeline: ordered.map(toTimelineEntry),
    };
}
export function summarizeExecution(events) {
    const classified = classifyExecution(events);
    return {
        executionId: classified.executionId,
        operationId: classified.operationId,
        ...(classified.tenantRef === undefined
            ? {}
            : { tenantRef: classified.tenantRef }),
        state: classified.state,
        reason: classified.reason,
        firstOccurredAt: classified.firstOccurredAt,
        lastOccurredAt: classified.lastOccurredAt,
        ...(classified.lastReceivedAt === undefined
            ? {}
            : { lastReceivedAt: classified.lastReceivedAt }),
        eventCount: classified.eventCount,
    };
}
dist/delivery-context.js
import { z } from 'zod';
import { opaqueReferenceSchema } from './schema.js';
/** Customer-owned durable metadata, NOT an Evidence wire payload or credential.
 * The observation decision is immutable for the execution. Old/missing context
 * is never upgraded to enabled during restore. Only trusted DB/outbox/job records
 * may supply this value; parsing does not authenticate its origin.
 */
export const deliveryEvidenceContextSchema = z.object({
    contextVersion: z.literal('nestarc.delivery.v1'),
    executionId: opaqueReferenceSchema,
    operationId: opaqueReferenceSchema,
    tenantRef: opaqueReferenceSchema,
    observation: z.enum(['enabled', 'interrupted', 'disabled']),
}).strict();
/** Validate only after reading an authenticated/trusted durable DB, outbox or job record.
 * This does not authenticate a caller or make request-supplied context trustworthy.
 * Missing, conflicting or old context disables observation without changing business work.
 */
export function contextFromTrustedRecord(record) {
    const parsed = deliveryEvidenceContextSchema.safeParse(record.evidenceContext);
    if (!parsed.success || parsed.data.executionId !== record.executionId ||
        parsed.data.operationId !== record.operationId || parsed.data.tenantRef !== record.tenantRef)
        return null;
    return Object.freeze(parsed.data);
}
dist/node.js
import { randomUUID } from 'node:crypto';
import { evidenceEventSchema, } from './schema.js';
import { deliveryEvidenceContextSchema } from './delivery-context.js';
export { contextFromTrustedRecord } from './delivery-context.js';
export { checkConnection } from './connection-check.js';
export const DEFAULT_COLLECTOR_URL = 'https://reliability.nestarc.dev';
// A cold HTTPS request plus the collector's bounded admission/authentication
// and persistence work can exceed two seconds. This is an export lifetime,
// never time awaited by the application's business operation.
export const EVIDENCE_EXPORT_TIMEOUT_MS = 5_000;
const producerStages = {
    relay: ['queue.enqueue'],
    worker: ['worker.result', 'webhook.request'],
    receiver: ['webhook.request', 'external.effect'],
};
function boundedInteger(value, fallback, maximum) {
    return value !== undefined && Number.isSafeInteger(value) && value > 0
        ? Math.min(value, maximum)
        : fallback;
}
function boundedPending(value) {
    return value !== undefined && Number.isSafeInteger(value) && value >= 0
        ? Math.min(value, 4_096)
        : 16;
}
/** A configuration error disables observation instead of failing business startup.
 * Tokens are sent only to the configured origin, never a redirect or URL suffix.
 */
function collectorEndpoint(value, allowLoopback) {
    try {
        const url = new URL(value ?? DEFAULT_COLLECTOR_URL);
        const transportAllowed = url.protocol === 'https:' || (allowLoopback && url.protocol === 'http:' &&
            ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname));
        if (!transportAllowed || url.username || url.password || url.search || url.hash || url.pathname !== '/')
            return null;
        return `${url.origin}/v1/evidence`;
    }
    catch {
        return null;
    }
}
export class EvidenceReporter {
    endpoint;
    ingestToken;
    fetchImpl;
    timeoutMs;
    maxInFlight;
    maxPending;
    maxObservationLifetimeMs;
    activeExports = 0;
    waitingExports = [];
    maxTrackedExecutions;
    executionTtlMs;
    executions = new Map();
    cleanupTimer;
    lastMonotonicTime = 0;
    closed = false;
    pendingEmits = new Set();
    losses = {
        invalid: 0, unregistered: 0, suppressed: 0, identity: 0, capacity: 0, expired: 0,
        closed: 0, credential: 0, rejected: 0, transport: 0,
    };
    transportFailures = {
        timeout: 0, network: 0, responseCleanup: 0,
    };
    constructor(options) {
        this.endpoint = collectorEndpoint(options.collectorUrl, options.allowLoopback === true);
        this.ingestToken = typeof options.ingestToken === 'string' && options.ingestToken.length <= 512 ? options.ingestToken : '';
        this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
        this.timeoutMs = boundedInteger(options.timeoutMs, EVIDENCE_EXPORT_TIMEOUT_MS, 10_000);
        this.maxInFlight = boundedInteger(options.maxInFlight, 1, 4_096);
        this.maxPending = boundedPending(options.maxPending);
        this.maxObservationLifetimeMs = boundedInteger(options.maxObservationLifetimeMs, 30_000, 30_000);
        this.maxTrackedExecutions = boundedInteger(options.maxTrackedExecutions, 4_096, 4_096);
        this.executionTtlMs = boundedInteger(options.executionTtlMs, 300_000, 300_000);
    }
    /** Silent by default. Adapters may record only these fixed diagnostic messages. */
    warn(message) { void message; }
    // Bare emit/report require admission; only the separate durable-context path
    // can restore enabled observations. Business work still receives an ID when
    // observation capacity is exhausted (persist a disabled downstream context).
    createExecutionId() {
        const executionId = randomUUID();
        this.pruneExecutions();
        if (!this.closed && this.executions.size < this.maxTrackedExecutions) {
            this.executions.set(executionId, {
                expiresAt: this.monotonicNow() + this.executionTtlMs,
                lastOccurredAt: 0,
                interrupted: false,
            });
            this.scheduleCleanup();
        }
        return executionId;
    }
    /** Snapshot the immutable downstream decision while the API owns admission.
     * Persist this with the operation/outbox. Null/legacy context stays disabled.
     * An interrupted execution must persist 'interrupted' BEFORE dispatching work.
     */
    createDeliveryContext(identity, interrupted = false) {
        this.pruneExecutions();
        const state = this.executions.get(identity.executionId);
        const parsed = deliveryEvidenceContextSchema.safeParse({
            ...identity, contextVersion: 'nestarc.delivery.v1',
            observation: !state || this.closed || this.endpoint === null || !this.ingestToken.trim() ? 'disabled' :
                interrupted || state.interrupted ? 'interrupted' : 'enabled',
        });
        if (!parsed.success) {
            this.drop('invalid');
            return null;
        }
        if (state) {
            if (state.restored || !this.matchesIdentity(state, parsed.data)) {
                this.drop('identity');
                return null;
            }
            if (state.deliveryContext && state.deliveryContext.observation !== parsed.data.observation) {
                this.drop('identity');
                return null;
            }
            state.identity = { operationId: identity.operationId, tenantRef: identity.tenantRef };
            state.deliveryContext = Object.freeze(parsed.data);
        }
        return Object.freeze(parsed.data);
    }
    /** Only call with context read from a trusted durable business record.
     * Restore is explicit; bare emit/report still cannot register missing IDs.
     * No historical event is synthesized and no send is retried.
     */
    emitDelivery(candidate, producer, input) {
        const context = this.restoreDelivery(candidate, producer, input.stage, input.status);
        if (!context)
            return Promise.resolve(false);
        return this.emit({ ...input, executionId: context.executionId,
            operationId: context.operationId, tenantRef: context.tenantRef });
    }
    /** Re-delivery of an existing observation must preserve its entire envelope. */
    reportDelivery(candidate, producer, event) {
        const parsed = deliveryEvidenceContextSchema.safeParse(candidate);
        if (!parsed.success)
            return this.drop('invalid');
        if (event.executionId !== parsed.data.executionId || event.operationId !== parsed.data.operationId ||
            event.tenantRef !== parsed.data.tenantRef)
            return this.drop('identity');
        if (!this.restoreDelivery(parsed.data, producer, event.stage, event.status))
            return Promise.resolve(false);
        return this.report(event);
    }
    restoreDelivery(candidate, producer, stage, status) {
        this.pruneExecutions();
        const parsed = deliveryEvidenceContextSchema.safeParse(candidate);
        const allowedStatus = producer === 'relay' ? status === 'succeeded' :
            producer === 'receiver' && stage === 'webhook.request' ? status === 'accepted' :
                status === 'succeeded' || status === 'failed';
        if (!parsed.success || !producerStages[producer]?.includes(stage) || !allowedStatus) {
            this.drop('invalid');
            return null;
        }
        const context = parsed.data;
        if (this.closed) {
            this.drop('closed');
            return null;
        }
        if (context.observation !== 'enabled') {
            this.drop('suppressed');
            return null;
        }
        let state = this.executions.get(context.executionId);
        if (state?.interrupted) {
            this.drop('suppressed');
            return null;
        }
        if (state?.deliveryContext && state.deliveryContext.observation !== 'enabled') {
            this.drop('suppressed');
            return null;
        }
        if (state && !this.matchesIdentity(state, context)) {
            this.drop('identity');
            return null;
        }
        if (!state) {
            if (this.executions.size >= this.maxTrackedExecutions) {
                this.drop('capacity');
                return null;
            }
            state = { expiresAt: this.monotonicNow() + this.executionTtlMs,
                lastOccurredAt: 0, interrupted: false, restored: true };
            this.executions.set(context.executionId, state);
            this.scheduleCleanup();
        }
        state.identity = { operationId: context.operationId, tenantRef: context.tenantRef };
        state.deliveryContext = Object.freeze(context);
        return context;
    }
    matchesIdentity(state, input) {
        return !state.identity || (state.identity.operationId === input.operationId && state.identity.tenantRef === input.tenantRef);
    }
    drop(reason) {
        this.losses[reason] = Math.min(Number.MAX_SAFE_INTEGER, this.losses[reason] + 1);
        return Promise.resolve(false);
    }
    getLossCounts() { return { ...this.losses }; }
    /** Subcounts of losses.transport, not additional losses or persistence receipts.
     * Fixed aggregate categories never contain event IDs, URLs or error contents.
     */
    getTransportFailureCounts() {
        return { ...this.transportFailures };
    }
    getQueueDiagnostics() {
        return { waitingExports: this.waitingExports.length, maxPending: this.maxPending,
            maxObservationLifetimeMs: this.maxObservationLifetimeMs };
    }
    emit(input) {
        this.pruneExecutions();
        const state = this.executions.get(input.executionId);
        if (this.closed)
            return this.drop('closed');
        if (state === undefined)
            return this.drop('unregistered');
        if (state.interrupted)
            return this.drop('suppressed');
        const next = Math.max(Date.now(), state.lastOccurredAt + 1);
        return this.report({
            schemaVersion: 'nestarc.evidence.v1',
            eventId: randomUUID(),
            occurredAt: new Date(next).toISOString(),
            ...input,
        });
    }
    interrupt(input) {
        void this.emit({
            ...input,
            stage: 'connector',
            status: 'interrupted',
            errorCode: 'CONNECTOR_INTERRUPTED',
        });
    }
    report(candidate) {
        this.pruneExecutions();
        const state = this.executions.get(candidate.executionId);
        if (this.closed)
            return this.drop('closed');
        if (state === undefined)
            return this.drop('unregistered');
        if (state.interrupted)
            return this.drop('suppressed');
        const parsed = evidenceEventSchema.safeParse(candidate);
        if (!parsed.success) {
            this.warn('Dropped invalid outbound evidence event');
            return this.drop('invalid');
        }
        if (!this.matchesIdentity(state, parsed.data))
            return this.drop('identity');
        // Restored producers cannot declare a new process-local interruption: its
        // durable execution-wide policy must be chosen before dispatch instead.
        if ((state.restored || state.deliveryContext?.observation === 'enabled') &&
            parsed.data.stage === 'connector')
            return this.drop('invalid');
        state.lastOccurredAt = Math.max(state.lastOccurredAt, Date.parse(parsed.data.occurredAt));
        // Suppression takes effect even if the interruption cannot be exported.
        if (parsed.data.stage === 'connector' &&
            (parsed.data.status === 'interrupted' || parsed.data.status === 'failed' ||
                parsed.data.errorCode === 'CONNECTOR_INTERRUPTED')) {
            state.interrupted = true;
        }
        if (this.endpoint === null)
            return this.drop('invalid');
        if (!this.ingestToken.trim()) {
            this.warn('Evidence export skipped: ingest credential is not configured');
            return this.drop('credential');
        }
        if (this.activeExports >= this.maxInFlight && this.waitingExports.length >= this.maxPending) {
            this.warn('Dropped evidence because the exporter is at capacity');
            return this.drop('capacity');
        }
        let resolve;
        const pending = new Promise(settle => { resolve = settle; });
        const item = {
            event: parsed.data,
            expiresAt: this.monotonicNow() + this.maxObservationLifetimeMs,
            finish: accepted => { this.pendingEmits.delete(pending); resolve(accepted); },
        };
        this.pendingEmits.add(pending);
        if (this.activeExports < this.maxInFlight)
            this.startExport(item);
        else {
            this.waitingExports.push(item);
            item.timer = setTimeout(() => {
                const index = this.waitingExports.indexOf(item);
                if (index === -1)
                    return;
                this.waitingExports.splice(index, 1);
                void this.drop('expired');
                item.finish(false);
            }, this.maxObservationLifetimeMs);
            item.timer.unref?.();
        }
        return pending;
    }
    startExport(item) {
        clearTimeout(item.timer);
        const remaining = item.expiresAt - this.monotonicNow();
        if (remaining <= 0) {
            void this.drop('expired');
            item.finish(false);
            return;
        }
        this.activeExports += 1;
        // No queued item re-enters report/restore or changes its source timestamps.
        // Already admitted observations also finish after close(), within their
        // original absolute lifetime. Failed sends are never queued a second time.
        void this.exportEvent(item.event, Math.min(this.timeoutMs, remaining)).then(accepted => {
            this.activeExports -= 1;
            item.finish(accepted);
            this.drainExports();
        });
    }
    drainExports() {
        while (this.activeExports < this.maxInFlight && this.waitingExports.length) {
            this.startExport(this.waitingExports.shift());
        }
    }
    async exportEvent(event, remainingMs) {
        const controller = new AbortController();
        let timeout;
        let timedOut = false;
        let responseReceived = false;
        const deadline = new Promise((_resolve, reject) => {
            timeout = setTimeout(() => {
                timedOut = true;
                controller.abort();
                reject(new Error('Evidence export deadline exceeded'));
            }, remainingMs);
            timeout.unref?.();
        });
        try {
            const exporting = (async () => {
                const response = await this.fetchImpl(this.endpoint, {
                    method: 'POST',
                    redirect: 'error',
                    headers: {
                        authorization: `Bearer ${this.ingestToken}`,
                        'content-type': 'application/json',
                    },
                    body: JSON.stringify(event),
                    signal: controller.signal,
                });
                responseReceived = true;
                // The collector body is unused. Both accepted and rejected responses
                // own the capacity slot until cancellation settles or the request is
                // aborted. Keep this in the raced operation so a late response from an
                // injected fetch that ignores abort still has its body discarded.
                await response.body?.cancel();
                if (controller.signal.aborted)
                    return false;
                if (!response.ok) {
                    this.warn(`Evidence collector rejected event (${response.status})`);
                    this.drop('rejected');
                }
                return response.ok;
            })();
            // Cancellation itself can reject or never settle; aborting the network
            // request must not make the business-facing promise wait on that cleanup.
            return await Promise.race([exporting, deadline]);
        }
        catch {
            this.warn('Evidence export failed open');
            this.drop('transport');
            const reason = timedOut ? 'timeout' : responseReceived ? 'responseCleanup' : 'network';
            this.transportFailures[reason] = Math.min(Number.MAX_SAFE_INTEGER, this.transportFailures[reason] + 1);
            return false;
        }
        finally {
            clearTimeout(timeout);
            controller.abort();
        }
    }
    async resetForTests() {
        if (process.env.NODE_ENV !== 'test') {
            throw new Error('Evidence reporter reset is only available with NODE_ENV=test');
        }
        await Promise.allSettled([...this.pendingEmits]);
        this.clearExecutions();
    }
    close() {
        this.closed = true;
        this.clearExecutions();
    }
    async flush() {
        // Admitted observations retain their absolute lifetime while waiting and
        // their remaining bounded deadline during network/body cleanup.
        await Promise.allSettled([...this.pendingEmits]);
    }
    /** Compatibility hooks for NestJS; ordinary Node applications use close/flush. */
    onModuleDestroy() { this.close(); }
    async beforeApplicationShutdown() { await this.flush(); }
    getDiagnostics() {
        this.pruneExecutions();
        return {
            activeExports: this.activeExports,
            pendingEmits: this.pendingEmits.size,
            trackedExecutions: this.executions.size,
            interruptedExecutions: [...this.executions.values()].filter(state => state.interrupted).length,
        };
    }
    monotonicNow() {
        this.lastMonotonicTime = Math.max(this.lastMonotonicTime, performance.now());
        return this.lastMonotonicTime;
    }
    pruneExecutions() {
        const now = this.monotonicNow();
        // Fixed TTL + monotonic admission preserve expiry order in insertion order.
        for (const [id, state] of this.executions) {
            if (state.expiresAt > now)
                break;
            this.executions.delete(id);
        }
        if (this.executions.size === 0 && this.cleanupTimer !== undefined) {
            clearTimeout(this.cleanupTimer);
            this.cleanupTimer = undefined;
        }
    }
    scheduleCleanup() {
        if (this.cleanupTimer !== undefined)
            return;
        const oldest = this.executions.values().next().value;
        if (oldest === undefined)
            return;
        this.cleanupTimer = setTimeout(() => {
            this.cleanupTimer = undefined;
            this.pruneExecutions();
            this.scheduleCleanup();
        }, Math.max(1, Math.ceil(oldest.expiresAt - this.monotonicNow())));
        this.cleanupTimer.unref?.();
    }
    clearExecutions() {
        clearTimeout(this.cleanupTimer);
        this.cleanupTimer = undefined;
        this.executions.clear();
    }
}
dist/connection-check.js
import { z } from 'zod';
import { opaqueReferenceSchema } from './schema.js';
const authorizedResponse = z.object({
    schemaVersion: z.literal('nestarc.connection-test.v1'),
    status: z.literal('authorized'),
    workspaceId: opaqueReferenceSchema,
    checkedAt: z.iso.datetime({ offset: false }),
    evidenceStored: z.literal(false),
}).strict();
/** A manual, bounded authorization/admission probe. It inserts no Evidence and
 * does not prove persistence, dashboard access, business completion or future health.
 */
export async function checkConnection(options) {
    let url;
    try {
        url = new URL(options.collectorUrl ?? 'https://reliability.nestarc.dev');
        if ((url.protocol !== 'https:' && !(options.allowLoopback === true && url.protocol === 'http:' &&
            ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))) || url.username || url.password ||
            url.search || url.hash || url.pathname !== '/' || !options.ingestToken?.trim() ||
            options.ingestToken.length > 512 || (options.expectedWorkspaceId !== undefined &&
            !opaqueReferenceSchema.safeParse(options.expectedWorkspaceId).success))
            return { status: 'invalid-configuration' };
    }
    catch {
        return { status: 'invalid-configuration' };
    }
    const controller = new AbortController();
    let timeout;
    const deadline = new Promise(resolve => {
        timeout = setTimeout(() => { controller.abort(); resolve({ status: 'unavailable' }); }, 5_000);
    });
    try {
        return await Promise.race([deadline, (async () => {
                const response = await (options.fetchImpl ?? globalThis.fetch)(`${url.origin}/v1/connection-test`, {
                    method: 'POST', redirect: 'error', signal: controller.signal,
                    headers: { authorization: `Bearer ${options.ingestToken}`, 'content-type': 'application/json' },
                    body: '{}',
                });
                if (response.status !== 200) {
                    await response.body?.cancel();
                    return { status: response.status === 401 ? 'unauthorized' : response.status === 403 ? 'not-ready' :
                            response.status === 429 ? 'limited' : 'unavailable' };
                }
                if (response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json' || !response.body) {
                    await response.body?.cancel();
                    return { status: 'invalid-response' };
                }
                const reader = response.body.getReader();
                const chunks = [];
                let bytes = 0;
                try {
                    while (true) {
                        const part = await reader.read();
                        if (part.done)
                            break;
                        bytes += part.value.byteLength;
                        if (bytes > 2_048)
                            return { status: 'invalid-response' };
                        chunks.push(part.value);
                    }
                }
                finally {
                    await reader.cancel();
                }
                const joined = new Uint8Array(bytes);
                let offset = 0;
                for (const chunk of chunks) {
                    joined.set(chunk, offset);
                    offset += chunk.byteLength;
                }
                let body;
                try {
                    body = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(joined));
                }
                catch {
                    return { status: 'invalid-response' };
                }
                const parsed = authorizedResponse.safeParse(body);
                if (!parsed.success || controller.signal.aborted)
                    return { status: 'invalid-response' };
                if (options.expectedWorkspaceId !== undefined && options.expectedWorkspaceId !== parsed.data.workspaceId) {
                    return { status: 'workspace-mismatch' };
                }
                return parsed.data;
            })()]);
    }
    catch {
        return { status: 'unavailable' };
    }
    finally {
        clearTimeout(timeout);
        controller.abort();
    }
}
dist/connection-check-cli.js
#!/usr/bin/env node
import { checkConnection } from './connection-check.js';
if (process.argv.length !== 2) {
    console.error('Use NESTARC_INGEST_TOKEN in the environment; this command accepts no arguments.');
    process.exitCode = 1;
}
else {
    const result = await checkConnection({
        ingestToken: process.env.NESTARC_INGEST_TOKEN ?? '',
        ...(process.env.NESTARC_COLLECTOR_URL ? { collectorUrl: process.env.NESTARC_COLLECTOR_URL } : {}),
        ...(process.env.NESTARC_WORKSPACE_ID ? { expectedWorkspaceId: process.env.NESTARC_WORKSPACE_ID } : {}),
        allowLoopback: process.env.NESTARC_ALLOW_LOOPBACK === '1',
    });
    // Never print credentials, workspace IDs, response bodies or transport errors.
    console.log(JSON.stringify({ status: result.status, evidenceStored: false }));
    if (result.status !== 'authorized')
        process.exitCode = 1;
}
Release manifest and file checksums
{
  "schemaVersion": "nestarc.sdk-release.v1",
  "package": "@nestarc/evidence",
  "version": "0.3.0",
  "publisher": "Nestarc",
  "archive": {
    "url": "https://reliability.nestarc.dev/downloads/nestarc-evidence-0.3.0.tgz",
    "path": "/downloads/nestarc-evidence-0.3.0.tgz",
    "bytes": 30459,
    "sha256": "4ffb06af8f2d5a4457238695e0010fd3fe6c79139579695b16e094e5166485c5"
  },
  "runtime": "Node.js 22.12 or later with ESM",
  "dependencies": {
    "zod": "^4.1.5"
  },
  "sourceScope": "The seven JavaScript runtime modules shipped inside this exact SDK archive. Type declarations, integration guidance and a local example are also included in the archive.",
  "sourceFiles": [
    {
      "path": "dist/index.js",
      "sha256": "50acf8552c73a58ba416e75da13c299545fe167cf673619d41756c158f072f6b",
      "bytes": 101
    },
    {
      "path": "dist/schema.js",
      "sha256": "a2e4423574ca3dc7670cc9930eeebe608620b1ed19b7dfc9d1c9f974ede1120b",
      "bytes": 7485
    },
    {
      "path": "dist/classifier.js",
      "sha256": "dbdbd27fc5cb3c35514f66c02c99ee390c7731f6ad1ae61e551afe33c03a682b",
      "bytes": 7343
    },
    {
      "path": "dist/delivery-context.js",
      "sha256": "39d5641ba32a51bba8994106541a3e98f9adf5c135f39e9fac39d0d50cfb614c",
      "bytes": 1348
    },
    {
      "path": "dist/node.js",
      "sha256": "6bee0e621f00b9ba424874201227182e6c78e0c701bede2bfa16e7ae7b69b420",
      "bytes": 19017
    },
    {
      "path": "dist/connection-check.js",
      "sha256": "8c6af5360f8ef05e27e4de1e2eb99d564740191427179df3013b79bc9a8fff96",
      "bytes": 4306
    },
    {
      "path": "dist/connection-check-cli.js",
      "sha256": "353084bdfbe33417a21cb6d92ffcc4dea42c58c5c37b59b2ab80713ddd33141d",
      "bytes": 884
    }
  ],
  "changes": [
    "Version 0.3.0 adds a bounded in-memory waiting queue: one active send and up to 16 waiting observations per reporter by default.",
    "Each observation keeps its event ID and original occurrence time. Its total lifetime, including queueing and sending, is capped at 30 seconds.",
    "A send remains bounded while its response body is cancelled. Timeout, network and response-cleanup losses have separate aggregate counters.",
    "Failed sends are not automatically retried. Evidence export does not determine your business result."
  ],
  "usage": {
    "termsUrl": "/terms/evaluation?releaseDigest=3a76f02c853fe0bf481b23d42dd8f3658a9e5e6075283dfa12c83fd9ecc31636",
    "termsVersion": "evaluation-2.0",
    "licenseDeclaredInPackage": false,
    "notice": "Nestarc distributes this SDK for connecting an authorized system to the service under its published beta terms. No separate open-source license is declared in the package."
  },
  "verificationScope": "The checksum identifies the SDK archive and the listed source files. It is not an independent security certification and does not pin the transitive dependencies installed by npm."
}

The archive declares zod ^4.1.5 as its npm dependency. The archive checksum does not lock every dependency npm installs.

No separate open-source license is declared in the package. Read the published beta terms (evaluation-2.0) for use with the service.

The checksum identifies the SDK archive and the listed source files. It is not an independent security certification and does not pin the transitive dependencies installed by npm.

2. Set the key in your server’s secret environment.

Set NESTARC_INGEST_TOKEN to the active key saved from Connect, then restart your application so it reads the value. Keep the key out of browser code, URLs, source control, and logs. Your existing webhook authentication uses a separate secret.

3. Observe your existing transaction and outbox.

Save this as delivery-evidence.mjs. In your HTTP handler, call observeOutboxCommit with your existing transaction function. That function must save the operation, outbox row, and supplied observation context together, and resolve only after the commit is acknowledged.

Node.js first evidence example
import { randomUUID } from 'node:crypto';
import { EvidenceReporter } from '@nestarc/evidence/node';

export const reporter = new EvidenceReporter({
  ingestToken: process.env.NESTARC_INGEST_TOKEN ?? '',
});

export async function observeOutboxCommit(commitOperationAndOutbox) {
  const identity = {
    executionId: reporter.createExecutionId(),
    operationId: randomUUID(),
    tenantRef: randomUUID(),
  };
  const evidenceContext = reporter.createDeliveryContext(identity);
  void reporter.emit({ ...identity, stage: 'http.request', status: 'started' });

  // Your function saves the operation, outbox, and this context atomically.
  // It must resolve only after the real COMMIT acknowledgement.
  const result = await commitOperationAndOutbox({ ...identity, evidenceContext });

  void reporter.emit({ ...identity, stage: 'database.commit', status: 'succeeded' });
  void reporter.emit({ ...identity, stage: 'outbox.write', status: 'succeeded' });
  return { result, executionId: identity.executionId };
}

This helper reports a request and a confirmed commit/outbox write; it does not report webhook success. It returns your transaction result and an opaque execution ID for finding the evidence. If your transaction fails, its error is preserved and no successful commit is reported.

4. Carry the same context to the relay and worker.

Read the saved identity and evidenceContext from your trusted outbox or job record. Use contextFromTrustedRecord and reporter.emitDelivery in each process. Keep disabled, interrupted, or missing context unobserved; do not create a replacement execution downstream.

  • After the queue acknowledges the existing dispatch, observe queue.enqueue / succeeded.
  • After the worker finishes, observe its actual worker.result.
  • Observe the webhook response separately. A timeout leaves the receiver’s outcome unknown.
  • Observe external.effect / succeeded only when the receiver’s business effect is explicitly confirmed.

An HTTP success response alone does not prove that the receiver’s business effect happened. If you cannot observe that effect, leave it unconfirmed.

Run the complete local delivery example for the relay, worker, and receiver insertion points. It uses synthetic data and needs no Nestarc account or key.

Keep your business operation independent.

Do not await evidence sends in business handlers. The SDK bounds its queue and send time and may drop observations. Use reporter.close() and await reporter.flush() only during graceful shutdown, after business handlers have settled.

Send allowed metadata and opaque references only. Never send payloads, headers, raw URLs, SQL, stack traces, email addresses, or customer identifiers.

03 / CHECK YOUR FIRST EXECUTION

Check the connection, then the evidence.

A working key and a visible execution are two separate checks.

  1. Check from the application host.

    With the server key set, run the command below. Optionally set NESTARC_WORKSPACE_ID to the workspace ID shown in Connect to check that the key belongs to the expected workspace.

    Connection check command
    npx --no-install nestarc-connection-check

    An authorized result with evidenceStored: false confirms reachability and current key authorization. This command creates no execution and does not confirm your business outcome.

    For other results, use the connection-check troubleshooting guide.

  2. Run one normal delivery through your application.

    Use a chosen test operation through the instrumented path. The observation calls must actually run; package installation and the connection check alone do not populate the dashboard.

  3. Check that execution in Connect.

    Copy the opaque executionId returned by the helper or saved in your outbox. In Connect, enter it under Check your trial execution. One manual read compares the starter reports with received evidence and identifies the next system and record to inspect. Choose Inspect this execution to open its full timeline. No business ID is needed; expired evidence is unavailable.

    The dashboard also supports Open by exact execution ID beyond its latest 50. The list does not update automatically. Both views make only manual reads. Collector receipt confirms that evidence reached Nestarc. Only an observed boundary confirms that boundary; it does not establish that the entire delivery succeeded.

If the list is still empty, check the active key and selected workspace, confirm the observation code ran, and review troubleshooting below. Do not replay a business operation merely to fill a gap in evidence.

LEARN BY INVESTIGATING

One delivery question.
A working example.

Use these optional examples to understand the observations or try the full path locally. Both are public and use synthetic data.

04 / READ THE TIMELINE

Find the last confirmed boundary.

Each view answers a different part of the delivery question.

Start with the execution assessment.

The classification describes what the collected evidence supports. The last confirmed boundary is the latest observed boundary with a source status of succeeded or accepted. Later work may have happened without evidence reaching the collector.

Follow the timeline, then open the evidence.

Select an observed event to inspect its allowed metadata, source stage, and source status. A visible response not observed label can describe a webhook timeout; the inspector preserves the original source status.

An external effect marked not observed is a display-only boundary, not a received event. It has no source status or timestamps. Missing evidence can reflect delay, instrumentation loss, exporter interruption, or unfinished work.

Source occurrence

When the source says the event happened. The timeline orders events deterministically by source time, with tie-breaking for events at the same time.

Collector receipt

When the collector received the evidence. Receipt time is shown separately; delayed or duplicate delivery does not turn it into the source occurrence time.

Freshness is a receipt signal.

Connector freshness uses the latest collector receipt in the loaded execution list. A recent receipt does not prove connector health or a downstream outcome. An old or missing receipt alone does not establish an interruption.

05 / UNDERSTAND OUTCOMES

Four examples.
Distinct evidence.

Scenario names describe deterministic synthetic fixtures. Dashboard classifications are derived from the evidence received.

Synthetic scenarioClassificationWhat it means
Complete deliveryDelivery confirmedAn explicit external effect succeeded event confirms the final delivery boundary.
Worker failedFailure reportedA worker result explicitly reports failure. Review the timeline for the reporting boundary.
Webhook response unknownOutcome unknownA webhook request was observed, but the external effect was not confirmed. A timeout does not prove that the receiver did nothing.
Evidence interruptedEvidence interruptedThe evidence stream explicitly reports an interruption before a final outcome is confirmed. Business work may have continued.

You may also see Outcome not yet confirmed when the evidence received so far confirms neither a webhook request boundary nor a final outcome. This is an unknown outcome, not a fifth demo scenario or a reported failure.

Scenario controls are local only.

The Run scenario control is available only in the local development UI for the synthetic demo workspace. The hosted dashboard reads evidence from your connected application; it does not expose scenario or business recovery controls.

06 / TROUBLESHOOTING

Check access.
Then check evidence.

Access and read errors are separate from delivery classifications.

What does my connection-check result mean?

The command prints status and evidenceStored: false. Only authorized exits successfully. Run it from the application host with its existing secret environment.

Scroll the table sideways to see the next step for each result.

StatusWhat it tells youNext step
authorizedThe host reached Nestarc and the key passed the current authorization and admission check.Run your chosen test operation, then Refresh the matching workspace. This check stores no Evidence.
invalid-configurationThe local key, collector origin, or expected workspace setting is invalid.Check NESTARC_INGEST_TOKEN in this process and NESTARC_WORKSPACE_ID if set. Use the default collector, or an HTTPS origin with no path, query, fragment or embedded credentials. Do not print secret values.
unauthorizedNestarc rejected the key.Check its status and expiry in Connect and which secret your process loaded. Replace a revoked or expired key and restart the affected processes.
workspace-mismatchThe authorized key belongs to a different workspace from the one you expected.Compare your intended workspace with Connect. Correct the selected key or NESTARC_WORKSPACE_ID; do not ignore the mismatch.
not-readyThe collector returned 403; collection is not currently permitted.Review current workspace access and beta terms. If those are valid, contact Nestarc support. A workspace owner cannot change Nestarc’s service configuration.
limitedA collection or shared admission limit rejected the check.Stop repeated checks. Review the limits in Connect and retry after current activity settles or the applicable time window resets. If the reason remains unclear, contact support; do not increase SDK concurrency.
unavailableThe request timed out, failed in transport, or received another unsuccessful HTTP response.Check outbound HTTPS access from this application host. If that works, retry the check later or contact Nestarc support. This result alone does not identify which side failed.
invalid-responseThe collector response could not be validated.Confirm the SDK version and collector origin, then contact Nestarc support if it persists. Do not treat an unrecognized response as a successful connection.

For a persistent error, use Nestarc support. Send the SDK version, status, time and timezone, and a short description. Never send the key, environment contents, payloads or raw logs containing customer data.

The connection is authorized, but observations are missing.

Confirm the calls ran for the chosen execution, then Refresh the matching workspace. Inspect the existing reporter in each participating process; a new reporter or another process cannot tell you what the original one sent.

Safe local SDK diagnostics example
// Use the existing reporter in this API, relay, worker, or receiver process.
console.info(JSON.stringify({
  source: 'nestarc-sdk-local',
  losses: reporter.getLossCounts(),
  transportFailures: reporter.getTransportFailureCounts(),
  queue: reporter.getQueueDiagnostics(),
  activity: reporter.getDiagnostics(),
}));

Use this as a manual diagnostic snapshot in your application’s operational code, outside the business transaction. These methods return fixed aggregate counts with no event identifiers or secrets. Counts are local to that reporter and reset when it is recreated; allow pending sends to settle before comparing snapshots.

  • credential or invalid: check configuration and allowed metadata.
  • unregistered, identity or suppressed: check trusted context and its observation decision. Do not turn disabled or interrupted context back on.
  • capacity or expired: the bounded queue or lifetime dropped observations. Reduce the trial volume and check collection limits; do not add automatic replays.
  • rejected: the collector declined an observation. Use the connection check and current limits to narrow the cause.
  • transport: inspect its timeout, network and responseCleanup subcounts. They are included in the transport total, not additional losses.
  • closed: observation was attempted after this reporter was closed. Review shutdown order.

A transport failure may happen after storage. Zero loss counts do not prove persistence or business success. Confirm received Evidence through the authenticated dashboard; never repeat a business operation just to replace missing observations.

I’m signed in, but have no workspace.

Open Connect to create a workspace for your application. If you need to join an existing workspace, its operator must grant you access. Logging in does not grant access to someone else’s evidence.

Access could not be checked.

Choose Try again to repeat the access check, and check your browser’s connection. If Nestarc remains unavailable, contact Nestarc support with the time, timezone and displayed error. A workspace owner can manage their keys and connection code, but cannot repair Nestarc’s service configuration. An access-check error does not describe the outcome of a business operation.

I’m asked to review the terms again.

Product access requires acceptance of the current evaluation terms. A previous login or acceptance of another version does not count as acceptance of the current version. You can still log out or read privacy information and support and rights guidance. Individual acceptance does not authorize an organization’s data processing.

The execution list is empty or a read failed.

Confirm the selected workspace and clear any search or classification filter. Filters search the loaded execution list, not all history. Use Refresh to read again, Try again after a list error, or Refresh execution evidence after a timeline error. An empty list does not prove that no business work occurred.

Check your connection and the collection limits shown during setup. Collection limits, delayed export, or expired evidence can leave the list empty. The published evaluation terms describe the applicable retention and beta scope; proposed paid plans do not change them.

Access changed or my session ended.

Log in again if prompted and select an approved workspace. Changing accounts or workspaces clears previous evidence from the view. If access was revoked, your workspace operator must grant it again.

Can I retry a delivery from the dashboard?

The dashboard reads evidence. Refresh and retry actions repeat read requests only; they do not retry, replay, or recover a customer business operation.

STOPPING YOUR TRIAL

Stop collection, then remove the code.

You can stop sending observations without closing your Nestarc account or changing a business operation.

  1. Revoke the trial’s ingest credentials.

    In Connect, check the workspace and revoke each key used by the trial. A revoked key cannot authorize new ingestion. Other processes using that same key also lose collection access; your customer business work continues.

  2. Remove observation code through your normal deployment.

    Remove the reporter calls, imports and secret configuration from the API, relay, worker and any receiver you instrumented. Remove the SDK dependency once nothing imports it. Preserve your existing transactions, queue behavior and webhook handling.

  3. Review observation-only stored fields last.

    Wait until all deployed producers and consumers no longer need the observation context before removing its columns or metadata fields through your normal migration process. Keep business and outbox records needed by your application. Nestarc does not perform this cleanup in your database.

Revocation and uninstalling do not delete Evidence already stored at Nestarc. It follows the published retention policy. To request personal account and owned-workspace closure, follow support and closure guidance: access and collection are blocked at acceptance; operator deletion and its final result are separate steps.