Skip to content
Console
Browse documentation
Reference

JavaScript SDK API

Functions, classes, options, parameters, and return values for the Anectico JavaScript and TypeScript SDK.

On this page

This is the application-developer API for @anectico/sdk 0.2.x. Import optional capabilities from their documented subpaths so replay, React, and provider-specific code stays out of the base bundle.

Use a key with ingest:write for traces, metrics, logs, and errors. Add analytics:write for identity, groups, diagnostic events, and flag exposure events; flags:read for server-side flag decisions (in a page or app, a client delivery key with only flags:evaluate instead); and replay:write for replay. In-product surveys use a separate key that holds only surveys:respond.

Initialize the core client

import { init, AnecticoClient } from '@anectico/sdk';

const anectico = await init({
  apiKey: process.env.ANECTICO_API_KEY, // native_ingest
  captureApiKey: process.env.ANECTICO_CAPTURE_API_KEY, // server_capture
  serviceName: 'orders-api',
});

// Equivalent lifecycle with explicit construction:
const second = await new AnecticoClient(options).start();
await second.stop();
API Parameters Returns Behavior
init(options?) AnecticoOptions Promise<AnecticoClient> Creates, validates, starts, and returns one client. Recommended entry point.
new AnecticoClient(options?) AnecticoOptions AnecticoClient Creates an unstarted client. Call start() before recording.
start() — Promise<AnecticoClient> Validates configuration and starts OTLP transports. Repeated calls return the same client.
flush() — Promise<void> Waits for in-flight identity posts and force-flushes trace, metric, and log providers without stopping them. Rejects on exporter failure. Concurrent calls share one flush.
stop() — Promise<void> Waits for bounded in-flight identify/group posts, flushes transports, removes installed hooks, and releases resources. Rejects on exporter failure; concurrent and repeated calls share the same shutdown result.
registerShutdownHook(hook) cleanup function void Registers a cleanup that stop() invokes once. Framework integrations use this to remove installed handlers.
isRunning() — boolean Reports whether the client currently accepts telemetry.
getConfig() — ResolvedConfig Returns a defensive copy of the resolved configuration for diagnostics. It includes sensitive headers; do not log it.
health() — HealthStatus Returns healthy, degraded, or stopped, recent internal errors, uptime, and signal counters.
stats() — ClientStats Returns processed, dropped, and failed-export counters plus timing data.

Keep one client per application process or browser page. Always await stop() during graceful server shutdown so identity posts and buffered telemetry finish before the process exits. A rejected identity endpoint response is reported through SDK debug logging instead of being treated as an accepted delivery. In managed mode, allow application-drain time plus up to twice shutdownTimeout: one deadline for identity and group delivery and one for concurrent trace, metric, and log provider shutdown.

Node cannot intercept SIGKILL, kill -9, a forced zero-grace pod deletion, or host loss, so these paths produce no terminal capture and run no final flush. The unexported tail can include active spans, in-flight identity and group requests, up to maxQueueSize ended spans and logs per batched signal (2,048 each by default), and metric observations since the last metricExportInterval. Prefer SIGTERM with a non-zero termination grace period.

For AWS Lambda-style runtimes, memoize one initNode() promise at module scope and await flush() in the handler's finally block. This exports the completed invocation before the runtime can freeze without disabling warm reuse. Keep end-user attribution request-scoped with contextWithDistinctId; reserve stop() for final process teardown.

Consent-first default since 0.2.0. initBrowser() collects nothing until your application grants a category. Upgrading from 0.1.x without calling setConsent (or passing a controller created with your existing grants) stops all browser telemetry. Browser clients also refuse openTelemetryMode: 'existing'. The package CHANGELOG.md lists every 0.2.0 change.

Most browser applications should call initApp() from @anectico/sdk/app, which performs the wiring below in one call and requires the consent grants as an argument; see Browser applications. The individual pieces remain available when you need to assemble them yourself.

Use the browser entry point with one shared controller. All four categories start denied; initBrowser() also creates an all-denied controller when collection is omitted. It is available as client.collection. Signal-enable options are additional switches, not consent.

import { initBrowser, createCollectionController } from '@anectico/sdk/browser';
import { AnalyticsClient } from '@anectico/sdk/analytics';
import { startReplay } from '@anectico/sdk/replay';

const collection = createCollectionController();
const client = await initBrowser({
  endpoint, apiKey: ingestKey, captureApiKey: browserCaptureKey, serviceName: 'storefront', collection,
});
const events = new AnalyticsClient({ endpoint, apiKey: browserCaptureKey, collection });
// Replay uploads go to the Console/API host (appEndpoint), not the ingestion endpoint.
const replay = startReplay({ endpoint: appEndpoint, apiKey: replayKey, collection });

// Apply your application's explicit consent decision. Omitted categories stay denied.
collection.setConsent({ diagnostics: true, performance: true, product: true });
events.identify('customer-123');
events.page('Pricing');

// A separate choice enables DOM recording.
collection.setConsent({ replay: true });
// Withdrawal immediately tears down this category and discards its unsent work.
collection.setConsent({ performance: false, replay: false });
Category Collection controlled by the grant
diagnostics Automatic uncaught errors, rejected promises and console logs; manual errors, messages, logs, user context and breadcrumbs.
performance Web Vitals, manual spans and metrics and browser fetch and XHR spans plus the traceparent propagation header.
product Capture and page events, identify and group events and their local identity changes in attached analytics clients; the visitor identifiers (distinct or anonymous id and session id) on every other signal.
replay DOM recording and screenshots. Replay console capture additionally requires diagnostics; replay network capture additionally requires performance.

collection.getConsent() returns a snapshot; collection.setConsent(partial) updates only named boolean grants. Repeating an unchanged grant does not start duplicate observers. Attach the same controller to every client that should follow that choice. Standalone clients constructed without it remain independently controlled. Nothing here disables application-owned monitoring libraries. If your application deliberately enables diagnostics before other choices, pass createCollectionController({ diagnostics: true }) explicitly; there is no automatic exemption.

Withdrawal stops owned observers, listeners, and timers, removes HTTP patches, discards unsent category queues and prevents their retry or replay after immediate regrant. An already-dispatched request can finish and reach storage; withdrawal cannot recall it or erase stored data. A new grant starts fresh collection on the same attached clients. Replay restarts with a full snapshot when its console or network permission changes, discarding the prior mixed buffer. events.disable() and replay.discard() are terminal; recreate those clients if you use those methods. Ordinary replay(), events.stop() and client.stop() are final-flush shutdowns for authorized work, not withdrawal. Stop all three when tearing down the integration.

The distinct or anonymous id and the session id are visitor identifiers and follow the product grant. Without it, errors, logs, spans and Web Vitals are sent without anectico.distinct_id or session.id, outbound requests carry no anectico.distinct_id baggage, and the SDK neither creates nor stores an anonymous id. Grant product when those signals must link to a person or a session.

When the page becomes hidden, and on every pagehide (including a back and forward cache one), the browser client flushes the categories that are still granted with keepalive requests kept below the browser's 64 KiB keepalive limit. The final Web Vitals snapshots of a closing page are therefore sent immediately rather than at the next periodic metric export. A withdrawn category has nothing to flush.

Consent-managed OTLP exporters use isolated providers and do not replace the application's OTel globals. They make one bounded HTTP attempt per export, without automatic transport retries. initBrowser requires openTelemetryMode: 'managed'; it refuses existing because Anectico cannot clear queues it does not own. Node's existing-provider integration remains available.

Web Vitals measurements

Granting performance with enableMetrics enabled starts the pinned Web Vitals 6.2.2 algorithms inside an SDK-owned disposable lifecycle. It reports web_vital.cls, web_vital.fcp, web_vital.lcp, web_vital.ttfb and web_vital.inp when the native browser APIs support them. CLS is unitless (1); all timing values use milliseconds (ms). FID and experimental soft-navigation metrics are not emitted. Unsupported or unobserved measurements are absent, never fabricated as zero. INP needs a qualifying interaction; LCP may finalize on interaction or when the page hides; CLS and INP may update on subsequent hidden transitions.

Each metric carries web_vital.sample_id, web_vital.lifecycle_id, web_vital.update, web_vital.navigation_type, web_vital.rating, web_vital.unit, web_vital.library_version and web_vital.value_type=lifecycle_snapshot. Multiple updates of one sample are snapshots, not multiple page views: select its latest update before computing population statistics. A back and forward cache restore receives a fresh lifecycle and sample identity; suspension keeps the client running after flushing. Terminal pagehide flushes and then performs best-effort shutdown.

Collection reads the browser's native buffered timings only after a grant. Those timings can predate the grant within the current page; a Web Vital describes the page lifecycle, not only the elapsed time since consent. Regrant may read the browser's retained timings as a new sample; it never resends the discarded SDK queue. No DOM elements, navigation URLs or attribution objects are included in the vital payload. Browser support is detected per metric: paint entries for FCP, navigation timing for TTFB, layout-shift for CLS, largest-contentful-paint for LCP, and Event Timing with interaction IDs for INP. Actual availability depends on browser version and page activity.

Pass browserRoutes to initBrowser to attach a declared route template as browser.route. Use the same templates as the page tracker below, and set browserRouting: 'hash' for a hash router. The initial route comes from the browser's recorded document-navigation URL. Missing or cross-origin navigation metadata leaves route attribution absent; hash mode also requires a recorded #/path. A later SPA transition does not move that document's LCP, CLS, and INP sample to another route. A bfcache restore snapshots the restored route for its new lifecycle. While product is granted, each lifecycle also carries its observed shared session.id; this is correlation context, not proof that a replay or trace is available.

Opt-in SPA page tracking

AnalyticsClient.trackPageViews() records the existing $pageview event after product consent. Declare non-personal route templates instead of collecting arbitrary path segments:

const routes = ['/', '/pricing', '/orders/:id', '/help/*'];
const client = await initBrowser({
  endpoint, apiKey: ingestKey, captureApiKey: browserCaptureKey, serviceName: 'storefront', collection, browserRoutes: routes,
});
const events = new AnalyticsClient({ endpoint, apiKey: browserCaptureKey, collection });
const pages = events.trackPageViews({
  routes,
  serviceName: 'storefront',
  campaigns: ['utm_source', 'utm_medium'], // optional; default is no query values
});
collection.setConsent({ product: true, performance: true });

Templates accept literal path segments, :parameters and a final *; a final * also matches its bare prefix, so /help/* covers /help and everything below it. They are case-sensitive; static matches take precedence over parameter matches. Supply 1–100 unique templates, each at most 256 characters. A known /orders/123 path emits /orders/:id; an unknown path emits the literal /[unmapped]. A query, fragment, dynamic parameter, page title or input value never becomes a page name. Template names themselves are application configuration: do not put personal values in them.

The default routing: 'history' records one current view when collection starts and one for each changed pathname after a successful pushState, replaceState or back and forward navigation. Same-path query and anchor changes and repeated callbacks do not create another view. Different parameter values can create distinct visits to the same template. routing: 'hash' uses #/path, excluding query and secondary-fragment data, and deduplicates overlapping history and hash notifications. Both automatic modes record a new view on bfcache restoration.

For a router with a committed-location callback, use manual mode:

const pages = events.trackPageViews({ routes, routing: 'manual', serviceName: 'storefront' });
// Call after each committed router transition, including the initial render.
pages.routeChanged(location.pathname, location.key);
// Repeated calls with the same key are ignored; a new key can represent a same-path visit.

Manual mode installs no history or navigation listeners and records only explicit calls. Keys are optional, contain 1–256 characters when supplied, and remain local. Without a key, consecutive identical paths are deduplicated. Automatic and manual tracking are alternative configurations for the same analytics client; stop the active tracker before changing options. Repeating the same configuration returns that tracker without another initial view.

The opt-in schema sets $page_name, $pathname and $navigation_type. Optional fields are $service_name, $referrer_origin and selected $utm_source, $utm_medium, $utm_campaign, $utm_term and $utm_content. Referrers default to HTTP(S) origin only, without credentials, path, query or fragment; set captureReferrer: false to omit them. Campaign keys default to none. Enabled values must occur once and contain 1–100 ASCII letters and digits, spaces or ._~-, without surrounding whitespace; other values are omitted. Do not put personal information in configured campaign values.

Page leaves

The same tracker records $pageleave when a view ends: on a route change, when the page is hidden, and on pagehide. It carries:

Property Meaning
$pathname, $page_name The declared route template that was left, never the URL.
$time_on_page_ms Milliseconds the page was visible, as an integer. Time while the tab was hidden is not counted.
$scroll_depth_percent The deepest point reached, 0–100: the bottom of the viewport as a share of the page height. A page that fits the viewport reports 100.
$is_only_pageview true when this was the only page view recorded in the current session so far. Use it for bounce rate.
$service_name Present when you set serviceName.

A page that becomes hidden reports its leave immediately and, if it becomes visible again, starts a new timing period on the same route, so one view can produce several $pageleave events. A hidden page that is then unloaded reports once. A bfcache restore records a new $pageview. Page-leave events are queued before the client's own page-exit delivery, which sends them with keepalive. $pageleave is an ordinary product event, billed like $pageview.

The helper creates no session or route observer while product collection is denied. Withdrawal removes its listeners and discards unsent page events through the analytics client's consent boundary. Regrant records only the current automatic-mode location; denied navigation is never replayed. Shared identity or session rules still apply, including explicit reset() on logout. pages.stop() removes tracking while preserving already queued authorized events; events.stop() or events.disable() also dispose the tracker. Explicit events.page() remains a separate named-call API, so do not additionally call it for transitions owned by this helper.

Opt-in autocapture

Autocapture, imported from @anectico/sdk/autocapture, records clicks, field changes and form submits without hand instrumentation. It is opt-in and starts nothing on import. It binds to the same product consent grant as page tracking: while that grant is denied no listener is installed, and withdrawal removes the listeners immediately and discards queued events.

import { AnalyticsClient } from '@anectico/sdk/analytics';
import { startAutocapture } from '@anectico/sdk/autocapture';

const events = new AnalyticsClient({ endpoint, apiKey: browserCaptureKey, collection });
const routes = ['/', '/pricing', '/orders/:id'];
events.trackPageViews({ routes });
const autocapture = startAutocapture(events, {
  routes,                       // required: the same declared templates as page tracking
  domEvents: ['click', 'submit'],
  blockSelectors: ['.checkout-card'],
  maskSelectors: ['[data-account]'],
});
collection.setConsent({ product: true });

autocapture.applyConfig({ enabled: false }); // pause without tearing the handle down
autocapture.stop();                          // remove every listener

startAutocapture(client, options) returns a handle. Starting again on the same client with the same options returns the running handle; different options throw until stop() is called. The handle has:

Member Behavior
stop() Removes every listener. Idempotent. The handle cannot be restarted; call startAutocapture again.
applyConfig(partial) Applies any of the configuration options below at runtime. Invalid values throw before anything changes. { enabled: false } removes the listeners while keeping the handle; { enabled: true } resumes (still subject to consent).
config() A copy of the configuration in force. With remoteConfig on, this is your configuration narrowed by the project's capture settings.
stats() Counters: captured and rateLimited by kind, blocked interactions, invalidSelectors dropped, faults contained, and remote (see server-controlled settings).
Option Default Meaning
routes required Declared route templates, as for trackPageViews.
routing 'history' 'history', 'hash' or 'manual'; how the current page is matched to a template.
serviceName none Sent as $service_name on every captured event. Use the value you pass to page tracking, so a page's views and its clicks answer to the same service filter. 1–128 bytes, no control characters.
enabled true Capture on or off.
domEvents ['click', 'change', 'submit'] Which events are observed. Rage-click detection needs click.
maskAllText false Omit visible text and the free-text attributes aria-label, title and alt everywhere.
maskSelectors [] CSS selectors for elements whose events are sent without text or attributes.
blockSelectors [] CSS selectors for elements that send no event at all.
capturePointer false Add click position data.
rateLimits see below Events per minute, by kind.
remoteConfig false true or { refreshIntervalMs }. Obey the project's capture settings.

The configuration options (everything except routes, routing and remoteConfig) are plain JSON values, so a configuration object can be applied later with applyConfig without changing your code. A selector the browser rejects is dropped and counted in stats().invalidSelectors; it never throws from an event handler.

Server-controlled settings

A project has capture settings that an administrator or an agent changes without a redeploy of your site. Pass remoteConfig: true and autocapture obeys them:

const autocapture = startAutocapture(events, {
  routes,
  blockSelectors: ['.checkout-card'],
  remoteConfig: true, // or { refreshIntervalMs: 60_000 }
});

remoteConfig is off by default, and without it autocapture behaves exactly as configured in code. It requires an AnalyticsClient: the settings are read from the same endpoint with the same key the client sends events with.

Capture is off until the settings have been read. Nothing is captured before the first successful read. If that read fails (the network is down, the key is refused, or the answer is not a settings document the SDK understands) capture stays off, and the SDK tries again at the next refresh. A project's settings start with autocapture off, so turn it on for the project before expecting events.

The settings narrow your configuration and never widen it. The configuration in force is, field by field, the more private of yours and the project's:

Option In force
enabled On only if both your code and the project's settings enable it.
domEvents The events in both lists.
maskAllText Masked if either masks.
capturePointer Collected only if both allow it.
maskSelectors, blockSelectors Every selector from both lists.
rateLimits The smaller limit for each kind.

Settings more permissive than your code change nothing: a project that allows change events does not add them to a site that configured domEvents: ['click']. applyConfig changes your side and the result is narrowed the same way.

Turning autocapture off for the project stops it. When a refresh brings enabled: false, the listeners are removed immediately. A refresh that fails keeps the settings already applied, so a network error never turns capture back on. A refresh that returns settings this version of the SDK cannot honour (for example a selector the browser rejects) turns capture off until a later refresh succeeds.

How long a change takes. A changed setting is served within 30 seconds. A page loaded after that uses it at once. A page that is already open picks it up at its next refresh: every five minutes by default, or the refreshIntervalMs you set (values under 30 seconds are raised to 30 seconds).

Consent. The read follows the product grant like the rest of autocapture. No request is made while it is denied, withdrawal cancels the refresh, and after a new grant the settings are read again before capture resumes. stop() ends the refresh.

stats().remote reports what happened:

Field Meaning
active remoteConfig is on for this handle.
applied Settings have been read and are in force. While false, capture is held off.
revision The revision of the settings in force.
fetched Reads that returned settings.
notModified Reads the server answered with "unchanged".
failed.network, failed.status, failed.invalid Reads that failed: no answer, an error status, or settings the SDK refused.

$autocapture is recorded for a trusted user click, change or submit. Events dispatched by scripts are ignored.

Property Value
$event_type click, change or submit.
$pathname The declared route template of the current page, or /[unmapped]. Never a URL, query or fragment.
$tag_name Lower-case tag of the element, resolved to the nearest button, link, label or other control for clicks.
$selector A stable CSS path: from the nearest id (or the top of the page), at most two classes per step, :nth-of-type among same-tag siblings.
$text Visible text of an interactive element (button, link, label, role=button and similar), whitespace-collapsed and truncated to 255 characters. Absent for other elements.
$attr_id, $attr_class, $attr_name, $attr_type, $attr_role, $attr_aria_label, $attr_title, $attr_alt, $attr_data_anectico_action The matching attribute, when present. No other attribute is ever read.
$elements_chain The element and up to nine ancestors, tag:attr="value";parentTag;..., limited to 2 KB.
$link_route or $link_origin For a link: the declared route template for a same-site link, or the origin only for an external http(s) link. Never the address itself; other schemes add nothing.

$rageclick is recorded in addition to the $autocapture clicks when an element receives three physical mouse, touch or pen clicks within one second and 40 pixels. Keyboard and assistive activations without a physical pointer do not contribute to a rage-click burst. It carries the same properties as $autocapture for that element plus $click_count, and at most one per element every five seconds. If click friction is running, a burst on an element it labels with data-anectico-action is reported only by friction as $friction; every other element is reported as $rageclick. Without friction running, $rageclick covers labelled elements too.

When capturePointer is on, mouse, touch and pen clicks also carry viewport-independent position data: $pointer_element_x_percent and $pointer_element_y_percent (position inside the element, 0–100), $pointer_document_x_percent and $pointer_document_y_percent (position on the whole page, 0–100) and $viewport_width_bucket, the lower edge in pixels of the viewport width class (0, 576, 768, 1024 or 1440).

Keyboard and assistive activations still count as clicks and carry the viewport width bucket, but omit pointer coordinates because they have no physical pointer location. They contribute to element counts and heatmap totals, and appear under without_position rather than in a grid cell. A physical click at coordinate zero keeps its position.

Rate limits. Autocapture is limited per minute by kind and overall. Defaults: { global: 300, click: 120, change: 60, submit: 30, rageclick: 10 }. Refill is continuous; a limit of 0 blocks that kind. Events over a limit are not queued and are counted in stats().rateLimited.

Privacy.

  • Values are never read: not from input, textarea or select elements, nor from contenteditable content. Text is never taken from inside an editable region, password or hidden fields, fields with autocomplete of cc-* or one-time-code, or elements whose name or id suggests a secret (password, token, card, PIN, one-time code and similar). Such elements are treated as masked.
  • Text is read only from interactive elements, never from arbitrary containers, and skips form controls, editable regions, scripts and masked or blocked descendants.
  • Attributes are allowlisted. value, href, src and every unlisted attribute are never read.
  • The page path is always a declared route template. Query strings and fragments never leave the browser.
  • Add data-anectico-block (or the class anectico-block or rr-block) to an element: that element and everything inside it produces no event. Add data-anectico-mask (or anectico-mask or rr-mask): events are still sent, with the tag name, a position-only selector and element chain, but no text and no attributes. Both apply to the element and its descendants. Click friction honours the same markers and suppresses masked actions, whose labels are private. Replay and page snapshots honour both marker sets too. Pass the same local maskSelectors and blockSelectors to each collector; server capture settings do not configure separately started replay or friction.
  • A fault inside the SDK is contained and counted in stats().faults; it never reaches the page.
  • A server rejects an event whose property names look like personal data and redacts values that match known personal-data patterns. Treat that as a backstop, not the control.

Browser performance budget

For production browser applications, keep the core SDK's incremental compressed transfer at or below 250 KiB and load the optional replay subpath only when recording is enabled; its incremental compressed transfer budget is 200 KiB. Against an otherwise identical control build, target no more than a 20% matched-workload regression, no more than 200 ms total blocking time, no task above 100 ms, and no SDK request amplification above 5% of application requests. These are acceptance ceilings, not expected steady-state values. Measure cold and warm builds on representative long pages and interaction bursts, and withdraw the relevant collection grants when consent changes. Use final-flush stop() only for ordinary integration teardown.

Replay uses ordinary fetches for size- and timer-triggered chunks so large initial snapshots and short activity bursts are not rejected by the browser's shared keepalive-body quota. The final pagehide tail alone uses a keepalive request because it may need to outlive the document.

AnecticoOptions

Queue sizes and stack-frame limits must be safe integers; timeouts must be finite and positive. Endpoints must be absolute HTTP(S) URLs without credentials, query strings, or fragments. An explicitly empty service name is invalid even when the environment contains a name. Authentication headers override custom headers case-insensitively.

Explicit options override environment values. Required values have no usable default. Keys must differ and have valid opaque key syntax. Intake verifies their actual purpose and project; local syntax validation does not prove authority. Browser bundlers pass the two keys explicitly. Telemetry-only clients may omit captureApiKey. A non-empty core identify/group call without it throws before changing identity or making a request; product-consent denial still returns without collecting. getConfig() includes both secrets, so do not log its output.

Option Type Default and environment Purpose
collection CollectionController Browser: all denied; core and Node: unset Shared category grants and disposal.
apiKey string ANECTICO_API_KEY; required Project-scoped native_ingest key for OTLP.
captureApiKey string ANECTICO_CAPTURE_API_KEY; empty Separate server_capture key (Node) or browser_capture key (browser) for core identify/group. No fallback to apiKey.
serviceName string OTEL_SERVICE_NAME; required Logical application or service name.
serviceVersion string OTEL_SERVICE_VERSION; 0.0.0 Deployed version.
environment string ANECTICO_ENVIRONMENT; runtime-derived Deployment environment such as production.
release string ANECTICO_RELEASE, then real serviceVersion Release used for regression detection and source maps.
dist string ANECTICO_DIST; empty Artifact and build discriminator within a release.
endpoint string ANECTICO_ENDPOINT; https://api.anectico.com Base API and OTLP URL.
protocol 'http' http JavaScript supports OTLP over HTTP only.
openTelemetryMode 'managed' | 'existing' ANECTICO_OTEL_MODE; managed Use existing when the application already owns global providers, exporters, instrumentation, and their lifecycle.
enableTraces boolean ANECTICO_ENABLE_TRACES; true Enable trace export.
enableMetrics boolean ANECTICO_ENABLE_METRICS; true Enable metric export.
enableLogs boolean ANECTICO_ENABLE_LOGS; true Enable log export.
enableErrorCapture boolean ANECTICO_ENABLE_ERROR_CAPTURE; true Enable captureError and captureMessage.
enableFetchTracing boolean true Browser only: instrument fetch and XHR and inject trace headers. Disabling it does both.
propagateTraceHeaderCorsUrls (string | RegExp)[] empty Browser cross-origin destinations allowed to receive trace and identity headers.
propagateTraceHeaderUrls (string | RegExp)[] ANECTICO_PROPAGATE_TRACE_HEADER_URLS; empty Node HTTP(S) destinations allowed to receive trace and identity headers.
ignoreIncomingRequestPaths string[] ANECTICO_IGNORE_INCOMING_REQUEST_PATHS; empty Node managed mode: exact or trailing-* paths excluded from generic incoming HTTP spans, such as health and internal control routes.
traceSampleRate number OTEL_TRACES_SAMPLER_ARG; 0.1 production, 1 development Trace sampling in the inclusive range 0–1.
errorSampleRate number ANECTICO_ERROR_SAMPLE_RATE; 1 Non-fatal captured-error sampling in the inclusive range 0–1.
attachStackTrace boolean ANECTICO_ATTACH_STACK_TRACE; true Parse and attach error stack frames.
maxStackTraceFrames number ANECTICO_MAX_STACK_TRACE_FRAMES; 50 Maximum captured frames.
batchSize number OTEL_BSP_MAX_EXPORT_BATCH_SIZE; 512 Maximum items per export batch.
batchTimeout number OTEL_BSP_SCHEDULE_DELAY; 5000 ms Maximum delay before a trace and log batch flushes.
maxQueueSize number OTEL_BSP_MAX_QUEUE_SIZE; 2048 Queue capacity before signals are dropped. Must be at least batchSize.
exportTimeout number OTEL_BSP_EXPORT_TIMEOUT; 30000 ms Per-export deadline.
metricExportInterval number OTEL_METRIC_EXPORT_INTERVAL; 60000 ms Metric push interval; raised to at least exportTimeout.
shutdownTimeout number OTEL_BSP_SHUTDOWN_TIMEOUT; 5000 ms Per-phase graceful deadline: bounds one-shot identity and group delivery and the concurrent provider-shutdown phase.
resourceAttributes Record<string,string> empty Extra OpenTelemetry resource attributes.
headers Record<string,string> empty Extra headers on exports. Do not expose secrets in browser builds.
debug boolean ANECTICO_DEBUG; false Enable SDK diagnostic logs.

In existing mode, Anectico creates no provider, exporter, propagator, or auto-instrumentation and does not flush or shut down application-owned providers. Start the application's OpenTelemetry SDK first. Anectico's manual APIs use its registered globals; the application remains responsible for provider flush and shutdown and for installing both W3C Trace Context and W3C Baggage propagation. Add the exported BaggageSpanProcessor and BaggageLogRecordProcessor to application-owned providers before startup when request-scoped identity should be materialized on spans and logs.

Traces, metrics, logs, and AI operations

Method Parameters Returns Behavior
tracer property OpenTelemetry Tracer Access the underlying tracer for advanced instrumentation.
startSpan(name, options?) name; OTel SpanOptions OTel Span Starts an inactive span; call span.end(). To parent subsequent operations, use client.tracer.startActiveSpan() with a context manager (initNode() installs one in managed Node mode). Returns a no-op span before startup.
recordMetric(name, value, labels?, unit?) metric name; number; string labels; optional OTel unit void Records one histogram observation. Invalid calls increment dropped-metric stats rather than throwing.
logEvent(level, message, attrs?) debug | info | warn | error | silent; message; attributes void Emits one structured OTel log. Complex attribute values are JSON-stringified.
recordLLMCall(options) LLMCallOptions void Records one completed model call as a gen_ai client span.
recordToolCall(options) ToolCallOptions void Records one completed agent tool and function call.
startAgentRun(options) AgentRunOptions { span, context, end } Starts an invoke_agent span. Run child work in the returned OTel context, then call run.end(status, reasonCode); the first valid outcome wins.

Agent-run status is completed, failed, timed_out, cancelled, or max_steps. The reason is a lowercase code matching [a-z][a-z0-9_]{0,63}; free text is rejected. Calling run.span.end() directly bypasses the terminal contract and produces only the legacy inferred status.

LLMCallOptions.model is required. Optional fields are responseModel, provider, operation, token counts (inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWriteTokens), costUsd, finishReason, startTime, endTime, and isError. messages and output opt into prompt and completion content capture.

ToolCallOptions.name is required. Optional fields are type, callId, conversationId, agentName, isError, arguments, and result. Arguments and results can contain sensitive data; omit them unless content capture is approved.

AgentRunOptions.agentVersion is optional. Set it to the release label you deploy. The SDK records it as the standard gen_ai.agent.version span attribute; it does not reuse service.version, because the hosting service and the agent can be released independently.

Context assembly events

The run handle exposes recordContextAssembly(value), recordContextCompaction(value), and recordContextCache(value). Call them inside context.with(...) while a recording model span is current and carries gen_ai.operation.name = chat | text_completion | generate_content | embeddings | fetch_response. The SDK throws for the run span, a tool span, an ended span, or no active span; it validates every field before adding the one event, so a rejected value records nothing.

const run = client.startAgentRun({ agent: "support-planner" });
await context.with(run.context, async () => {
  const model = client.tracer.startSpan("chat gpt-4o", {
    kind: SpanKind.CLIENT,
    attributes: { "gen_ai.operation.name": "chat" },
  });
  await context.with(trace.setSpan(context.active(), model), async () => {
    run.recordContextAssembly({
      budgetTokens: 8192,
      reservedOutputTokens: 1024,
      assembledTokens: 6144,
      overflow: "none",
      assemblySha256,
      sources: [{ kind: "memory", id: "memory:item-7", sensitivity: "confidential",
        tokens: 512, position: 3, visibility: "included" }],
    });
  });
  model.end();
});
run.end("completed");

See Context events for exact bounds. Never send compaction ratio; the server derives it. OpenAI and Anthropic wrappers add cache hit or miss events when cache fields are present. A reported zero is a miss, while an absent field produces no event.

Errors, messages, users, and breadcrumbs

Breadcrumbs snapshot nested JSON data when added. If optional data cannot be serialized, the breadcrumb is retained without that data so error reporting can still include the trail.

Method Parameters Returns Behavior
captureError(error, options?) Error; CaptureOptions string Sends an error span and returns its generated ID. Returns '' when capture is unavailable or sampled out.
captureMessage(message, level?, options?) message; ErrorLevel; CaptureOptions string Captures a diagnostic message. Only error and fatal produce error status.
addBreadcrumb(category, message, options?) strings; BreadcrumbOptions void Buffers context for the next captured error, then clears the buffer. Capacity is 100.
setUser(user) AnecticoUser void Sets global error user context. Prefer identify for the cross-signal identity spine.
clearUser() — void Removes global error user context.

CaptureOptions accepts tags: Record<string,string>, extra: Record<string,unknown>, user, stackTrace, level, and fingerprint. A fingerprint is a string or string array; commas are the component delimiter, and {{default}} includes automatic grouping. Tag keys are encoded as indexed anectico.tag.* telemetry attributes automatically; the capture level is indexed as error.level for Issue filtering and context.

AnecticoUser accepts id, email, username, ipAddress, segment, and data. Breadcrumb options accept level and data.

Identity and accounts

Logout rotates the live anonymous identity and session even when browser storage cannot be updated. Session storage failures fall back to stable memory state; a clock moving backward also starts a fresh session. Rotating sessions releases their previous ownership markers.

Both the core client and AnalyticsClient clear cached groups when identify() changes the distinct ID, including at the first anonymous-to-identified transition. Identifying the same ID preserves groups. Call group() after identifying to associate subsequent events with the new person's account. Already-captured events retain their original identity and groups during retries; explicit per-event $groups still override the cache.

Method Parameters Returns Behavior
identify(distinctId, properties?) stable ID; person properties void Switches shared identity and starts a best-effort identify request that stop() waits for. Empty IDs are ignored. Requires captureApiKey unless product consent is denied.
group(groupType, groupKey, properties?) group type; stable key; group properties void Associates the current person with an account and starts a $groupidentify request that stop() waits for. Requires captureApiKey unless product consent is denied.
reset() — void Logout: stops active replay recorders and flushes their prior-user tail before rotating anonymous identity or session; also clears groups, global error-user context, and uncaptured breadcrumbs. Other already-captured telemetry remains queued with its original attribution.
getSessionId() — string Returns the current browser tab's cross-signal session ID, rotating it after its idle or max duration.

For concurrent Node requests, use contextWithDistinctId(ctx, id) and distinctIdFromContext(ctx) instead of changing process-wide identity. adoptDistinctIdFromBaggage must be used only after an authenticated gateway has replaced untrusted inbound baggage.

Browser session state is tab-scoped. A reload keeps the same session, while a separate, duplicated, or opener-created tab receives its own session and replay boundary. Call identify after authentication in every tab. Calling reset on logout rotates only that tab's identity or session and does not move another live tab's telemetry into the new customer's session. Replay's next chunk index is stored beside this tab-scoped state so a reload appends to the recording instead of replacing its first chunks.

Diagnostic events: @anectico/sdk/analytics

Capture snapshots event properties when enqueueing. Later mutations do not change queued events. Properties must be JSON-serializable; invalid events are rejected before they enter the queue. The built-in analytics and feature-flag HTTP clients do not follow redirects: configure the final API endpoint directly so project credentials stay at that endpoint.

import { AnalyticsClient } from '@anectico/sdk/analytics';

const events = new AnalyticsClient({
  endpoint: 'https://api.anectico.com',
  apiKey: process.env.ANECTICO_CAPTURE_API_KEY!, // server_capture (browser_capture in a browser)
});

release and appVersion are stamped as the reserved $release/$app_version string properties on every capture()'d and page()'d event when configured, never invented when they are not. An explicit option wins; otherwise Node falls back to process.env.ANECTICO_RELEASE/ANECTICO_APP_VERSION, and a browser falls back to <meta name="anectico-release" content="…">/<meta name="anectico-app-version" content="…"> tags your build tooling injects. See Reserved event properties.

API Parameters Returns Behavior
new AnalyticsClient(options) endpoint, API key, optional collection, flushAt, flushInterval, requestTimeoutMs, fetchImpl, identity, session, onDeliveryError, release, appVersion AnalyticsClient Creates a batching event client. Defaults: flush at 20 events or 5 seconds; 10-second request timeout.
getDistinctId() — string Current known or anonymous ID of this client's identity: the shared one unless you passed identity.
capture(event, properties?) name; properties void Queues one product event with identity, groups, session, timestamp, and dedup ID.
captureAndAck(event, properties?, messageId?) name; properties; optional dedup ID Promise<CaptureAndAckResult> ({ messageId, delivered, rejected? }) Sends one event immediately (bypassing the batching queue) and resolves only once the platform has acknowledged it. Returns the exact message_id used, for a downstream call keyed on delivery — such as a product experiment outcome's event_id. Uncertain delivery throws CaptureAndAckError with messageId; retry that ID on the same client to resend the original envelope.
captureRevenue(event, { amount, currency, properties? }) name; amount in major units; three-letter upper-case currency; optional properties void Queues one product event carrying $revenue and $currency, the properties revenue analytics reads by default. Throws before queueing an amount that is not a finite number, has more than four decimal places or exceeds 99,999,999,999.9999 in size, or a currency that is not three upper-case letters. See Send revenue.
page(name, properties?) explicit page name; optional properties void Queues $pageview with $page_name, using the same capture identity, consent and retry behavior. Does not collect browser URL, title or referrer.
trackPageViews(options) BrowserNavigationOptions, including declared routes BrowserNavigationController Opts in to consent-owned history and hash tracking or explicit router commits. The controller exposes routeChanged(path?, navigationKey?) and stop(). See the SPA schema above.
identify(distinctId, personProps?) ID; person properties void Queues identity merge, switches identity, and starts a flush.
group(type, key, properties?) group type and key; properties void Records membership and a group identify event.
reset() — void Rotates the anonymous identity and the session this client holds: the ones you passed as identity and session, otherwise the shared ones. A sibling client with its own is untouched; events already recorded keep their identity and session. See request-scoped clients.
flush({ keepalive? }?) optional browser keepalive Promise<boolean> Validates the complete indexed acknowledgement for each chunk. Resolves true only when every attempted event was queue-acknowledged or the queue was empty; false when explicit refusals were dropped without pending retries; rejects while uncertain or retryable events remain.
stop({ keepalive? }?) optional browser keepalive Promise<boolean> Closes admission, stops timers and listeners, and flushes remaining batched events while already-admitted immediate requests settle. Resolves true only if that drain was fully acknowledged; false for refusals, failed immediate requests, or discarded immediate retry envelopes. Rejects while retryable batched events remain queued, after all active requests settle. Discards unresolved captureAndAck envelopes as client_stopped. Concurrent and repeated calls share the shutdown result.
disable() — Promise<void> Immediately blocks new capture, page, identify, and group calls, removes delivery triggers and discards pending analytics. Waits for already-dispatched delivery to settle without retrying uncertain events. Terminal for this instance.
stats() — { recorded, delivered, dropped, queued } Delivery counters. delivered counts validated queue acknowledgements, dropped counts explicit refusals and local discards including withdrawal, and queued includes events awaiting an acknowledgement or retry.

AnalyticsClient holds one customer at a time: capture has no per-call person, and group attaches later events of that one customer. A Node.js server that captures product events for many customers creates one client per request, as the next section shows and the runnable Node example does for identity.

Request-scoped clients on a Node server

AnalyticsClient holds one customer and one session. In a browser that is the visitor and the tab's session, which every client on the page shares on purpose. A Node.js server that captures product events for many customers at once builds one client per request instead, with its own identity and its own session, both taken from your server's own session and never from the request body:

import {
  AnalyticsClient,
  IdentityManager,
  SessionManager,
  memoryStorage,
} from '@anectico/sdk/analytics';

function eventsFor(request: { visitorId: string; userId?: string; sessionId: string }) {
  // The visitor cookie is this request's anonymous ID.
  const identity = new IdentityManager({
    getItem: () => request.visitorId,
    setItem: () => {},
    removeItem: () => {},
  });
  if (request.userId) identity.identify(request.userId);
  // A session of this request's own: private storage, and the session ID your server already trusts.
  const session = new SessionManager({ storage: memoryStorage(), newId: () => request.sessionId });
  return new AnalyticsClient({
    endpoint: process.env.ANECTICO_CAPTURE_URL!,
    apiKey: process.env.ANECTICO_CAPTURE_API_KEY!, // server_capture
    identity,
    session,
    flushInterval: 0, // deliver with flush() or stop() before the response ends
  });
}
Rule Behavior
session The SessionManager the client stamps as session_id on every event it sends: capture, page, captureRevenue, captureAndAck, identify and group. It is bound when the client is constructed. A value that is not a SessionManager throws a TypeError.
Default Without session the client uses the shared session, the same one every other such client uses. In a browser that is the tab's session. On a server it is one session for the whole process, whatever identities you inject, so give each request its own.
storage A SessionManager created without storage falls back to that same shared store and is not isolated. Pass memoryStorage() for a private one.
newId Mints each new session ID, a random UUID by default. Return the session ID your server already trusts to bind it. It is asked again after reset() and after 30 minutes without an event or 24 hours, so a function that always returns the same ID gives the same session back.
Events already recorded Identity, groups, session, timestamp and properties are serialized once, when the call is made. A queued event retries with exactly those values and its message ID, and an unresolved captureAndAck resends the same bytes, even after another request or a reset().
reset() Rotates the anonymous identity and ends the session of this client's own identity and session, and no other. A sibling request is untouched, and so is the shared session. The next event starts the new anonymous identity and asks newId for a new session. A client given no session ends the shared one, as a browser logout does.
Consent, counters, shutdown Withdrawal, stats(), immediate request settlement, the flush() and stop() outcomes and retry admission belong to the instance and do not depend on which session it holds.

Call await events.stop() before the response ends: it delivers queued identify and group calls, resolves true only if the drain was fully acknowledged, and ends this client only. A browser page leaves session out so that its clients keep sharing the tab's session. The Go and Python SDKs name the person and account on each event instead.

Send revenue

captureRevenue is an ordinary capture() with the amount in $revenue and the currency in $currency, the two properties revenue analytics reads by default. It uses the same identity, groups, session, consent gate, queue, retry and delivery accounting as capture(), and sends nothing when collection is not allowed.

events.captureRevenue('purchase', { amount: 19.99, currency: 'USD', properties: { plan: 'pro' } });
events.captureRevenue('refund', { amount: -5, currency: 'USD', properties: { plan: 'pro' } });
Option Rule
amount A finite number in major units of the currency (19.99, not 1999), negative for a refund, with at most four decimal places and a size of at most 99,999,999,999.9999. A sum such as 0.1 + 0.2 (0.30000000000000004) has too many places and throws.
currency Three upper-case letters, such as "USD".
properties Further properties of the event, such as plan. $revenue and $currency are set last, so they win over same-named keys.

The call validates before queueing and throws, instead of sending a value that revenue analytics would count as invalid and leave out of every figure. Amounts are never rounded or converted. Other SDKs send $revenue and $currency as ordinary properties on any event.

Page tracking is opt-in: call events.page('Pricing', { plan: 'free' }) when your application decides a page view occurred, including after a client-side route change. This method does not observe navigation or read browser URL, title or referrer; trackPageViews() is the separate opt-in helper for that workflow. The explicit name overrides a $page_name supplied in properties. Choose stable names and pass only properties approved for collection; arbitrary supplied properties are not automatically redacted.

Capture uses the versioned indexed acknowledgement contract. HTTP 200 alone never establishes delivery. The SDK validates the version, request UUID, original indices, all outcomes and totals before updating counters. Queue acknowledgement does not prove unique stored rows, query visibility or completed identity changes.

flush() resolves false for explicit refusals, including invalid events or quota refusal. It rejects if uncertain events remain, including network failures, malformed responses at any HTTP status, and typed retryable request failures. A single flush can contain both dropped events and an uncertain remainder: inspect stats() and onDeliveryError even when the promise rejects. Accepted and refused siblings are never automatically retried. Uncertain events retain their original message ID, identity, timestamp and payload; unsent later chunks also remain queued. Retries wait at least one second and honor Retry-After up to 60 seconds. requestTimeoutMs defaults to 10,000 ms and must be positive; request dispatch and receipt reading each have a bounded deadline.

captureAndAck() retries are caller-driven. Retry an uncertain capture on the same AnalyticsClient instance with the same event name and messageId. The client retains the first call's serialized envelope, including its properties, timestamp, identity, groups and session. Later property mutations, identity changes and session rotations do not alter that retry. Concurrent calls for the same unresolved ID share one request, and retries count as one recorded event. Caller-supplied IDs must contain a non-whitespace character; blank IDs fail locally before recording or sending. Use a fresh ID for a different occurrence; an unresolved ID cannot be reused for a different event name. A definitive refusal resolves { delivered: false, rejected } and must not be retried with the same ID.

import { CaptureAndAckError, type CaptureAndAckResult } from '@anectico/sdk/analytics';

let result: CaptureAndAckResult;
try {
  result = await events.captureAndAck('order_completed', { amount: 42 });
} catch (error) {
  if (!(error instanceof CaptureAndAckError) || !error.retryable) throw error;
  // The generated ID identifies the original envelope retained by this client.
  result = await events.captureAndAck('order_completed', { amount: 42 }, error.messageId);
}
if (!result.delivered) handleCaptureRefusal(result.rejected);

Retained envelopes are in memory only and are removed after a definitive acknowledgement, collection withdrawal, disable() or stop(). Retry before closing the client or withdrawing collection; a page reload or new client cannot recover that envelope. flush() does not retry these immediate captures. At most 10,000 unresolved immediate captures are retained. When that limit is reached, new IDs fail locally; existing IDs remain retryable without losing their original envelopes. At most 10,000 immediate requests may be active, including requests still settling from an earlier consent grant. If those slots are full, a retained retry throws CaptureAndAckError with retryable: true without sending; retry its ID after an active request settles. stats().queued includes unresolved immediate captures. Withdrawal immediately counts prepared, unsent captures as dropped. Graceful stop() lets already-admitted immediate requests dispatch, even when called in the same turn as captureAndAck(), and waits within their request and receipt deadlines. The final batch starts while those requests settle. Shutdown reports false if an active immediate request fails, times out or is refused, or if a retained retry envelope is discarded; it never retries immediate captures. Already-dispatched requests still count validated acknowledgements or refusals; an uncertain result is discarded when it settles. The shutdown result describes work outstanding when stop() began; inspect stats() and onDeliveryError for earlier settled refusals or drops.

An in-flight failure after withdrawal or shutdown has retryable: false, because its original envelope has been discarded. CaptureAndAckError.retryable indicates retained retry state; it does not guarantee a later request will be accepted.

For shared browser consent, use collection.setConsent({ product: false }): it discards pending work and allows fresh collection on a later grant. Consent is checked before evaluating event properties and again before queue admission. Withdrawal during a property getter, enumeration or toJSON() discards the event, including after immediate regrant. A fresh call after regrant can collect normally. Events discarded before admission do not change stats(); identify() and group() also leave local identity unchanged when preparation loses its grant. Calling disable() during preparation also discards the event without throwing.

For permanent standalone withdrawal, use await events.disable() instead of a final-flush stop(). It immediately prevents new capture, page, identify and group calls from collecting data or changing identity, removes timer or page-exit delivery, and reports pending data as local collection_disabled drops. Already-dispatched requests may still be committed: valid acknowledgements retain their normal counters, while uncertain positions and unsent later chunks are discarded without retry. A concurrent flush resolves false if it discards work; an empty flush after disable performs no network activity. After disable settles, queued is zero. Repeated disable is safe; new consent requires a new AnalyticsClient. Reset is logout, not consent withdrawal. This controls only this analytics instance; separately stop replay and manage the other telemetry clients you initialized. Do not initialize collection before consent where your application requires it.

Refusals are reported once per affected chunk through onDeliveryError({ status, dropped, reason }), stats().dropped, and console.warn. Reasons include invalid_event, quota_exceeded, mixed_rejection, bad_request, unauthorized, forbidden, payload_too_large, and queue_overflow and collection_disabled (local discards use status 0). Unknown delivery alone does not fire a drop callback. A callback that throws is swallowed. A malformed or oversized acknowledgement never counts as accepted or definitely refused; response reads are bounded to 64 KiB and a deadline.

const events = new AnalyticsClient({
  endpoint: 'https://api.anectico.com',
  apiKey: process.env.ANECTICO_CAPTURE_API_KEY!, // server_capture (browser_capture in a browser)
  onDeliveryError: ({ status, dropped, reason }) => {
    myAlerting.warn(`anectico dropped ${dropped} events: ${status} ${reason}`);
  },
});

Explicit $groups in event properties override the current shared memberships. A membership change takes effect only after its event is successfully serialized and queued. Capture receipts distinguish accepted events from validation or quota drops, including partial acceptance in an HTTP 200 response. Counters and the delivery-error callback report only the rejected portion as dropped, and flush() resolves false for that partial outcome.

Analytics and feature-flag endpoints must be absolute HTTP(S) base URLs without embedded credentials, query strings, or fragments. Uncertain flag-exposure delivery can retry on the next eligible flag read; it never interrupts evaluation.

The diagnostic-event queue is bounded and in memory. A transient network failure is requeued with the same message ID and the timer retries while the page remains alive, so reconnecting without navigation preserves order and backend deduplication. A reload, navigation, browser termination, or OS process kill replaces that memory and can discard events that have not reached Anectico. Call await events.flush({ keepalive: true }) before controlled navigation when delivery matters; do not describe browser diagnostic events as durable offline storage.

Product experiment delivery: @anectico/sdk/node

experiments({ applicationKey, baseUrl }) builds a Node-only client for product experiment delivery — assign, expose and recordOutcome — over an APPLICATION-purpose key (purpose: application, experiments:deliver only). It throws immediately if called where window exists: an application key is a server-side secret, and this guard exists so it can never quietly ship inside a browser bundle.

import { experiments, ExperimentDeliveryError } from '@anectico/sdk/node';

const delivery = experiments({
  applicationKey: process.env.ANECTICO_EXPERIMENTS_KEY!, // an_... — server-side only
  baseUrl: 'https://app.anectico.com', // the control address, not the capture address
});

const assignment = await delivery.assign('checkout-copy', distinctId);
if (assignment.enrolled) {
  const exposure = await delivery.expose(
    'checkout-copy',
    assignment.assignmentId!,
    `checkout-copy:${assignment.assignmentId}`, // idempotency key
  );
  // ... later, after capturing the qualifying event with captureAndAck ...
  await delivery.recordOutcome('checkout-copy', exposure.exposureId, sourceId, eventId);
}

baseUrl is the control address, the one that serves your REST API; the capture address serves captureAndAck and is a different host. Record an outcome only with the messageId of a capture that resolved with delivered: true. Capture the qualifying event for overlapping requests with a client bound to each request's identity and session, as request-scoped clients shows; see also the experiment guide.

API Parameters Returns Behavior
experiments(options) applicationKey, baseUrl, optional requestTimeoutMs, fetchImpl ExperimentsClient Throws if window exists, or if applicationKey/baseUrl is missing.
assign(key, distinctId) experiment key; a distinct_id or person id Promise<{ enrolled, assignmentId?, arm?, payload?, revision? }> Stable per canonical person: aliases, retries and concurrent calls all return the same assignment. enrolled: false means serve the default experience — nothing more to do.
expose(key, assignmentId, idempotencyKey) — Promise<{ exposureId, exposedAt, replayed, arm }> Register the first exposure, before any treatment-dependent work. Safe to retry with the same idempotencyKey.
recordOutcome(key, exposureId, sourceId, eventId) eventId is a captured event's id (e.g. captureAndAck's messageId) Promise<{ receiptId, recordedAt, state, replayed, reason? }> Binds a captured event to the exposure as a provisional outcome receipt.

Every method throws ExperimentDeliveryError on failure, with status, code (the REST error code), an optional typed reason, and retryable. EXPERIMENT_SUBJECT_UNRESOLVED (the person is not identified) and EXPERIMENT_OUTCOME_CONFLICT (the captured event is already bound to a different exposure) are always retryable: false — no idempotency key makes either succeed on retry. See Product experiments for the full lifecycle, launch admission and how a result is read.

Feature flags: @anectico/sdk/analytics

Bootstrap data and returned payload containers are copied. Exposure deduplication distinguishes identity, flag name, and the response's type and value. Remote decision responses must contain one JSON document of at most 16 MiB, with boolean or string flag values. Invalid responses preserve the last valid cache; overlapping reloads allow only the latest invocation to replace it.

The FlagsClient option apiKey is a project-scoped management key with flags:read, so FlagsClient belongs on a server; a page or app evaluates named keys with a client delivery key instead (see Feature flags from a page or app). Automatic exposures require a separate captureApiKey: a server_capture key for Node or a browser_capture key for a browser, with analytics:write, belonging to the same project. A native capture key cannot authorize flag decisions, and a management key cannot authorize native capture.

For the hosted endpoints, configure both origins:

import { FlagsClient } from '@anectico/sdk/analytics';

const flags = new FlagsClient({
  endpoint: 'https://app.anectico.com',
  apiKey: process.env.ANECTICO_FLAGS_API_KEY!, // flags:read
  captureEndpoint: 'https://api.anectico.com',
  captureApiKey: process.env.ANECTICO_CAPTURE_API_KEY!, // server_capture
});
await flags.reload('user_8842');
const showNewCheckout = flags.isFeatureEnabled('new-checkout');

captureEndpoint defaults to endpoint for an endpoint serving both routes; the capture key never falls back to the decision key. Without an explicit capture callback, construction throws if captureApiKey is missing, blank, or equal to apiKey. This applies to bootstrap-only clients too, because reading a bootstrapped flag also records an exposure.

An explicit capture(event, properties, distinctId) callback remains the exposure transport and does not need captureApiKey. Use it to route exposures through an existing AnalyticsClient with its collection consent and delivery handling, or pass () => {} for decision-only use. Keep that event client's identity consistent with the ID passed to reload. Browser applications using initApp already route exposures through their shared product-event client.

The default exposure sender validates capture receipts: only QUEUED confirms acceptance. UNKNOWN, a lost response, a timeout, or an invalid receipt retains the exact original event, including its message ID, identity, timestamp and properties. A later read of the same flag and value retries it after exponential backoff (one second initially, capped at 30 seconds), honoring Retry-After up to one minute. It does not run a background retry timer; retained events live only in this client until reset() or an identity-changing reload() clears them.

A validated receipt reporting an invalid event, exhausted quota or request refusal discards the exposure and reports it once through onDeliveryError({ status, dropped: 1, reason }), including HTTP 200 item refusals. Further reads remain deduplicated until reset or an identity change. Listener errors never interrupt flag evaluation. Requests and receipt reads share a 10-second timeout by default, and receipts are limited to 64 KiB. When you supply a capture callback, that callback owns delivery and its diagnostics; use the analytics client's stats() and onDeliveryError for a batched sender.

API Parameters Returns Behavior
new FlagsClient(options, capture?) decision endpoint and key; separate capture key and optional capture endpoint; optional fetch and bootstrap, requestTimeoutMs, onDeliveryError or exposure callback FlagsClient Creates an in-memory decision client. Bootstrap avoids the first decision request; timeout and delivery diagnostics apply to automatic exposure transport. The callback overrides that transport.
reload(distinctId, personProperties?, groups?) person ID; properties; group map Promise<void> Calls /api/v1/decide, replaces cached flags and payloads, and resets exposure dedup when identity changes.
getFeatureFlag(key, { testCaseId }?) flag key; optional test-case ID boolean | string | undefined Returns a decision and emits one deduplicated exposure.
getFeatureFlagDetails(key, { testCaseId }?) flag key; optional test-case ID FlagEvaluationDetails Returns the value with its exact definition_version, evaluation_status, is_fallback, mode and input_context, and records the same fields on the exposure.
isFeatureEnabled(key) flag key boolean True for true or a non-empty string variant.
getFeatureFlagPayload(key) flag key unknown Returns the payload from the latest decision.
allFlags() — Record<string, boolean | string> Returns a copy of every cached decision.
hadEvaluationErrors() — boolean Reports whether the server used a fallback during the latest evaluation.
reset() — void Clears decisions, payloads, evaluation metadata, exposure deduplication and retained retries. Invalidates a pending reload. Call reload before serving flags again.

The application facade also clears flag decisions after an admitted identify, on logout and on shutdown. A reload begun for the previous identity cannot populate the next person's cache. Reload explicitly with the current identity before reading flags after these boundaries.

Browser decisions reveal flag configuration to the client. Never store secrets in flag rules or payloads. Use a flag-read key only on a server; a browser must not receive it.

Exposure evidence

Each served read records $feature_flag_called with the flag's evaluation evidence: the exact definition version ($feature_flag_version, only for a successful evaluation), the status, whether the value is a fallback, remote or local, an optional testCaseId and an input context with the evaluation basis, whether every targeting input was supplied, and an input_binding hash of the explicit inputs (project, flag key, distinct ID, person properties and groups). A remote and a local read with the same explicit inputs produce the same binding. Two reads of the same value under different flag definitions are two exposures. A bootstrapped value without decision metadata records only the key and value. See feature-flag exposure verification for the contract and the read that checks it.

For Node and server local evaluation, import LocalFlagsClient from @anectico/sdk/flags-local and pass {endpoint, apiKey, projectId} plus an optional capture callback in the same shape accepted by FlagsClient. refresh() validates a project-bound snapshot with ETag/If-None-Match; repeated refreshes are serialized. evaluate(key, distinctId, personProperties?, { testCaseId }?) returns {value?,payload?,snapshot_version?,reason,error?} plus definition_version, evaluation_status (evaluated, evaluation_error, unavailable_flag for a missing, stale or unknown snapshot entry, unsupported_targeting for a cohort, group or pinned-audience target), is_fallback, mode and input_context, and never returns a value from a stale snapshot or an unsupported target. Evaluated reads emit $feature_flag_called with the same evidence once per identity, key, value, version, test case and input binding when a callback is configured; call reset() after an explicit identity reset. A network-backed callback also needs analytics:write.

Local API Parameters Returns Behavior
new LocalFlagsClient(options, capture?) options: endpoint, API key, canonical project ID, optional fetch and clock; capture(event, properties, distinctId) LocalFlagsClient Creates one exact-project cache. Snapshot reads need flags:read; an Anectico-backed capture callback needs analytics:write.
refresh() — Promise<"updated" | "not_modified"> Fetches or conditionally revalidates the strict, bounded snapshot without replacing a last-known-good cache on failure.
evaluate(key, distinctId, personProperties?, { testCaseId }?) flag key, exact identity, person properties, optional test-case ID LocalEvaluationResult Evaluates only a fresh snapshot and emits a successful exposure through the optional callback.
reset() — void Clears local (identity,key,value) exposure deduplication; preserves the snapshot.

In-product surveys: @anectico/sdk/surveys

Surveys ask your visitors short questions inside your product. You create and launch a survey in Anectico; the SDK asks which surveys the current visitor may see on the current page, shows one, and records what happened as events on the person. Who is eligible (audience, flag, sample, schedule, response cap, cooldown) is decided on the server. The browser receives only the questions.

Nothing starts on import. Surveys follow the client's product consent grant like autocapture: while it is denied nothing is requested and nothing is shown.

import { AnalyticsClient } from '@anectico/sdk/analytics';
import { startSurveys } from '@anectico/sdk/surveys';

const routes = ['/', '/billing', '/orders/:id'];
const events = new AnalyticsClient({ endpoint, apiKey: browserCaptureKey, collection });
events.trackPageViews({ routes });
const surveys = startSurveys(events, {
  endpoint: 'https://app.anectico.com',  // the host flag decisions use
  apiKey: surveysKey,                    // holds only surveys:respond
  routes,
  environment: 'production',
});
collection.setConsent({ product: true });

The key and the endpoint

Surveys talk to the same host as flag decisions (https://app.anectico.com), not the capture host. Create a dedicated client delivery key that holds only surveys:respond (apikey create --purpose client_delivery --scope surveys:respond) and pass it as apiKey. That key is public by construction (it is in your page), so it must not carry anything else: surveys:respond can ask which surveys a visitor may see and record a display, a response or a dismissal, and nothing more. It cannot read a survey's targeting or results, change a survey, or capture events. Do not reuse your capture key or any management key. A key bound to one project uses that project. A key without surveys:respond is refused with HTTP 403; surveys then show nothing and the refusal is counted in stats().failed.status.

Options

Option Default Meaning
endpoint required The app host, an absolute http(s) URL without credentials, query or fragment.
apiKey required A key holding only surveys:respond. Never returned by any getter and never part of an event.
routes required Declared route templates, the table page tracking uses. Only a template is sent as the page (/orders/:id, never /orders/8123); an undeclared path is sent as /[unmapped], which matches only surveys that target every page.
routing history history, hash or manual. With manual the SDK installs nothing and you call refresh(path) after a route change.
environment none Sent with every display, response and dismissal, for example production. At most 200 bytes.
render true false turns off the default widget: use onSurvey and the headless handle.
position bottom-right bottom-right or bottom-left.
delayMs 0 Milliseconds between a survey becoming eligible and showing.
onSurvey none Called once per survey, when it is offered. With render: false this is how your own UI learns of it.
distinctId, groups the client's Override current identity or groups for eligibility. Each offer stays bound to its original identity; old operations close after identify, reset or consent boundaries.
sessionId the client's collected session Optional source function. Its exact value is frozen at each first display, response or dismissal attempt; omission means no collected session. Retries keep the original value.
debounceMs 300 Route changes within this window cause one read.
minRefreshIntervalMs 5000 The minimum gap between reads. It can be raised, never lowered below 5 seconds.
requestTimeoutMs 10000 Per-request timeout.
retry {maxAttempts: 5, baseDelayMs: 1000} Attempts per delivery (1 to 10) and the first backoff, which doubles up to 30 seconds.
labels English Replace any widget string (see below).
thankYouMs 5000 How long the thank-you state stays; 0 keeps it until closed.

Starting again on the same client with the same options returns the running handle; different options throw until it is stopped. Invalid options throw before anything starts.

The handle

Member Returns Behavior
stop() void Removes the widget, the navigation listeners and every timer; restores history only if the SDK's own wrapper is still the current one. Idempotent; the handle cannot be restarted.
refresh(path?) Promise<void> Asks again, coalesced and never sooner than the minimum gap. In manual routing pass the committed path. Never rejects.
eligible() Survey[] The surveys eligible now (a copy). Empty while consent is denied or a read has failed.
shown(survey) Promise<SurveyReceipt> Records that you displayed the survey (headless only; the widget does it itself).
submit(survey, answers) Promise<SurveyReceipt> Validates, then sends the answers.
dismiss(survey) Promise<SurveyReceipt> Records that the visitor closed the survey without answering.
stats() SurveysStats active, eligible, fetched, failed: {network, status, invalid}, offered, shown, responded, dismissed, retries, gaveUp, refused, faults.

A Survey is {id, revision, questions}. A question has id, type (single_choice, multiple_choice, rating, open_text), prompt, required, and options ({id, label}) or scale ({min, max, lowLabel, highLabel}). A SurveyAnswer is {questionId} plus optionIds, rating or text for the question's type. A SurveyReceipt is {at, replayed}; replayed is true when the server had already recorded the same call. A real server timestamp and a boolean replay status are required; normal protobuf omission of false means false. Malformed receipts are SURVEY_INVALID_RESPONSE, with uncertain acceptance. Retry the same operation and answers while its offer remains valid. After an uncertain response, changed answers are refused locally. SURVEY_IDENTITY_COVERAGE_UNSUPPORTED refuses complete delivery identity coverage beyond 100 linked IDs. It is separate from a valid empty offer or an outage. Session context connects accepted outcomes to the source session; it does not establish recording availability or stored-event visibility.

When it reads

The SDK reads eligibility when it starts and after each route change to a different declared route, never more often than every 5 seconds. A read that fails, times out, or returns something this SDK cannot fully understand shows nothing (it fails closed) and is counted in stats().failed. While a survey is open there is nothing to ask, so route changes do not read. After a survey is answered or dismissed, the next route change reads again.

A survey is offered to a person once: after the display is recorded the server stops returning it, which is why the SDK keeps the survey until it has an outcome. Only one survey is ever open, and a survey the visitor closed is not shown again.

Headless use

With render: false the SDK reads eligibility and calls onSurvey; you draw the UI and report what happens:

const surveys = startSurveys(events, {
  endpoint, apiKey: surveysKey, routes, render: false,
  onSurvey: (survey) => {
    myUi.open(survey, {
      onDisplayed: () => surveys.shown(survey),
      onSubmit: (answers) => surveys.submit(survey, answers), // throws SurveyError
      onClose: () => surveys.dismiss(survey),
    });
  },
});

Call shown when the survey is actually on screen. submit checks the answers the way the server will before sending anything: every required question answered, at most one answer per question, option ids the question declares (exactly one for single choice), a rating that is an integer inside the scale, and open text of 1 to 2000 characters after line breaks and tabs become spaces and the ends are trimmed (control characters are refused). An empty answer to an optional question is left out. A failed check throws SurveyError with code SURVEY_ANSWER_INVALID and sends nothing.

Errors. Every failure is a SurveyError with code, message, retryable and, when the server answered, status. A retryable server response can also include retryAfterMs; retries honour that advice, bounded to one minute:

code Meaning
SURVEY_ALREADY_ANSWERED, SURVEY_ANSWER_INVALID, SURVEY_NOT_ACTIVE, SURVEY_REVISION_MISMATCH, SURVEY_RESPONSE_CAP_REACHED, SURVEY_NOT_OFFERED The server refused. The survey is over for this visitor: it is removed from eligible() (and the widget closes), and the call is not retried.
SURVEY_NETWORK The request failed or timed out, or the server answered 408, 429 or 5xx. Retried, then reported once attempts are used up.
SURVEY_REJECTED The server refused with a status retrying cannot change, such as 403 for a key without surveys:respond.
SURVEY_INVALID_RESPONSE The answer to a delivery was not a complete valid receipt, including a real timestamp and a boolean replay status. Retried with the same operation id.
SURVEY_UNAVAILABLE Nothing was sent: surveys are stopped, consent is denied, the survey was not offered, or another survey is open.

Retries and ids. Each display, response and dismissal carries an id the SDK generates once and reuses on every retry, so the server records it once. A failed submit keeps the answers: after a network failure, 429 or 5xx the SDK retries with the same response id and exponential backoff up to retry.maxAttempts, then rejects with SURVEY_NETWORK and increments stats().gaveUp. Calling submit again with the same answers reuses the same id; different answers start a new response.

A receipt preserves the server timestamp and replayed boolean. Normal omission of false is accepted as false; a malformed timestamp or a replay value of another type is uncertain delivery. Eligible documents are accepted whole: at most three offers, with prompts up to 500 characters and option or scale labels up to 200 characters. Offers contain their id, revision and questions.

The default widget

With render on (the default) the SDK shows an accessible panel in the bottom corner of the page, one question per step with a progress indicator. It is not a modal: the rest of the page stays usable and no focus trap is installed. It is a Shadow DOM element appended to document.body, so your styles do not leak in and its styles do not leak out. It needs no external fonts, images or network requests.

  • Single choice is a radio group, multiple choice is a set of checkboxes, rating is a labelled scale of buttons from min to max with the low and high labels, and open text is a text area with a character counter.
  • Next, Back and Submit move through the steps; a required question must be answered to continue. The close button (and Escape inside the panel) dismisses the survey. After a successful submit a short thank-you state shows and the panel closes after thankYouMs.
  • If a send fails the panel keeps the answers, says so, and Submit tries again with the same response id.
  • The display is recorded when the panel is on screen. A visitor who closes it is recorded as a dismissal. Withdrawing consent removes the panel without recording anything.
  • Accessibility: role="dialog" named by the current question, focus moves into the panel on open and back to where it was on close, every control is a native, keyboard-operable element with a visible focus ring, prompts and options use dir="auto", animation stops under prefers-reduced-motion, it works down to 320 px wide, and the default colours meet WCAG 2.1 AA contrast.
  • Survey text is written by your team and is always shown as plain text; it never becomes markup.

Style it with CSS custom properties on the host element, [data-anectico-survey]:

Property Default Controls
--anectico-survey-bg #ffffff Panel background.
--anectico-survey-fg #1a1a1a Text colour.
--anectico-survey-muted #4a4a4a Secondary text.
--anectico-survey-accent #1d4ed8 Buttons, selected state, focus ring, progress.
--anectico-survey-accent-fg #ffffff Text on the accent colour.
--anectico-survey-border #767676 Panel and control borders.
--anectico-survey-radius 12px Corner radius.
--anectico-survey-font system UI stack Font family.
--anectico-survey-offset 16px Distance from the screen edge.
--anectico-survey-width 360px Panel width, capped to the viewport.
--anectico-survey-z 2147483000 Stacking order.
[data-anectico-survey] {
  --anectico-survey-accent: #0f766e;
  --anectico-survey-radius: 4px;
}

When you change colours keep text at 4.5:1 contrast and the border at 3:1. The position option chooses the corner. labels replaces any string: next, back, submit, close, sending, thankYou, required, pleaseAnswer, tooLong, invalidText, sendFailed, somethingWrong, and the templates progress (Question {current} of {total}) and characters ({count} / {max}).

  • While the product grant is denied there is no request. Withdrawal removes the panel, cancels pending reads, delays and retries, and discards a response still in flight. A later grant reads again.
  • Each read sends the visitor's current distinct id, the page as a declared route template and, when you set any, the group memberships. Deliveries send the survey id and revision, the id, the distinct id, environment when set, and for a response the answers. Never a URL, a query string or any other page content.
  • Every request uses redirect: "error". The key is sent only in the X-API-Key header.
  • The outcomes are recorded as events on the person: a display, a dismissal, and one event per answer. Free-text answers pass through the same personal-data redaction as other captured text, so ask for no sensitive details.

Problem reports: @anectico/sdk/problem-reports

Use the same project-scoped browser_capture key as capture (or mobile_capture in a mobile client) for ProblemReportsClient({ endpoint, apiKey }); it can submit reports but cannot list, read or delete them. Set endpoint to the Console and API origin. Keep the prepared report's UUID stable when retrying. See Problem reports for the widget, custom forms and bounds.

Replay: @anectico/sdk/replay

startReplay(options) returns a synchronous stop function with sessionId, discard(), diagnostics() and attachProblemScreenshot(reportId, png). With a collection controller, rrweb starts only when replay is granted; sessionId is empty before the first grant. Ordinary stop flushes the authorized buffered tail; discard() stops permanently without a flush. Both are idempotent.

stopReplay.diagnostics() returns { recorder: { name: '@rrweb/record', version, source: 'bundled' } }. The version identifies the recorder actually installed when the SDK was built, rather than a dependency range. It is available before replay consent without starting recording and contains no keys, person identifiers or recorded content. The recorder is bundled locally; replay does not load a separate recorder script from a CDN.

Browser request tracing and replay network capture isolate instrumentation failures from application requests. A failed consent check disables observation for that request. Setup failures pass the original arguments through, and errors after dispatch never trigger a second send. Fetch responses, XHR events and native failures, including abort reasons, retain their application behavior. Request bodies, including FormData and streams, are never read or re-encoded by these wrappers.

AnecticoClient.reset() also stops every active recorder synchronously and flushes its buffered tail before changing the shared identity or session. This prevents a recorder started for user A from capturing user B's UI after logout. Start a new recorder after identifying user B if replay should resume.

Replay chunks are best-effort and buffered only in page memory. A failed chunk upload is dropped rather than retried, and an offline reload can lose the buffered tail. Replay must never be used as a durable audit log.

Option Type and default Purpose
collection optional CollectionController Gate recording and auxiliary category permissions; resume on a later grant.
endpoint, apiKey required strings Replay endpoint and key with replay:write.
sessionId, distinctId optional strings Override shared session or person linkage.
maxEvents number; 50 Flush threshold.
flushMs number; 5000 Time-based flush interval.
maskAllInputs boolean; true Mask input values in the DOM recording.
maskSelectors, blockSelectors string arrays; [] Additional subtree rules, using the same selector options as autocapture, friction and page snapshots. Invalid replay selectors refuse recording.
maskAllText boolean; false Withhold DOM text and identifying attributes throughout the recording.
slimDOM true | 'all'; unset Remove non-visual scripts, comments, and head metadata from DOM snapshots. Use 'all' for the smallest initial snapshot.
inlineStylesheet boolean; rrweb default true Inline linked stylesheets into snapshots. Set false when the replay viewer can load the original stylesheets and snapshot size matters.
captureConsole boolean; false Explicitly opt into bounded source-redacted console events, with diagnostics consent when a controller is supplied.
captureNetwork boolean; false Explicitly opt into source-redacted fetch and XHR metadata and bodies, with performance consent when a controller is supplied.
maxBodyBytes number; 10240 Per-body truncation limit.
maxNetworkEvents, maxConsoleEvents number; 1000 Per-session event caps.
fetchImpl typeof fetch Test and SSR transport override.

shouldFlush(...) and buildChunkBody(...) are exported pure helpers for custom and testing use; most applications should call only startReplay.

Marked DOM text and attributes are withheld before replay chunks are buffered, including mutations. Auxiliary string redaction recognizes sensitive keys, credential assignments, Bearer tokens, JWTs and email addresses; it cannot classify all private prose. See record session replay safely for marker behavior, traversal budgets and verification of outgoing payloads.

After obtaining replay consent, initialize replay before mounting a very large DOM so the first visible state is recorded, then hydrate long feeds or tables progressively. For pages with hundreds of nodes, combine progressive rendering with slimDOM: 'all' and, where the viewer can load the original CSS, inlineStylesheet: false. This keeps recorder parse work and the initial snapshot off the longest main-thread task without reducing the recorded user journey.

Page snapshots for heatmaps: @anectico/sdk/page-snapshot

startPageSnapshots(options) takes one masked copy of a page per route template and viewport class, so a heatmap can be drawn over the page. It is a separate lazy entry point and loads nothing of the recorder. It returns { stop(), stats() }.

Nothing is captured unless all of these hold: your application started it, the project's page snapshot setting is on, the visitor granted replay, the page has loaded and stopped changing, the current path matches one of your routes, and the server answered that it wants a copy of this page at this viewport class. Most visits upload nothing.

The copy is taken in steps of a few milliseconds while the browser is idle, so a large page causes no long main-thread task. It is given up, and nothing is sent, if the route changes or consent is withdrawn before it finishes, or if it takes longer than ten seconds.

Option Values and default Purpose
endpoint, apiKey required strings The application endpoint and a key with only replay:write, as for replay.
routes required, 1 to 100 templates The same route templates as page tracking and autocapture. A copy is stored under the template.
routing 'history' (default) or 'hash' How your application routes.
serviceName string The same service name as page tracking and autocapture.
collection CollectionController Shares consent; without the replay grant nothing is captured.
maskAllText boolean; default true Replace every visible character by x.
maskSelectors, blockSelectors CSS selectors Extra areas to mask, or to replace by an empty box.
inlineImages boolean; default false Embed images the browser is allowed to read back.
distinctId string Defaults to the SDK's current identity.
idleMs number; default 1500 How long the page must make no DOM change, after load or a route change, before a capture.
maxWaitMs number; default 10000 The longest to wait for that quiet. A page that never settles is captured as it is then.
fetchImpl typeof fetch Test and SSR transport override.

The project's settings are a ceiling. Your options can only mask and block more: text is masked if either side masks it, the selector lists are combined, and images are embedded only if both allow it. If the project lists a selector this browser cannot parse, nothing is captured.

A copy is built only from a fixed list of ordinary page elements, layout attributes (class, style, sizes, table spans and a few states such as hidden and open) and style rules. Never copied, whatever the options: the value of an input, text area or select; the text of an option; an editable region's content; anything inside a blocked area; scripts, comments and event handlers; id, name, title, alt, placeholder, aria-label, data-* and framework attributes; quoted text in a style rule (such as content: "…"); fonts, animations, transitions, shadows and filters; and any address (src, href, srcset, or a stylesheet url(), other than an embedded image when images are allowed). A custom element is copied as a plain block with the same layout. A copy larger than 2 MiB is not sent, and the SDK checks each copy against the same list the server applies before sending it.

stats() returns counters: offers, wanted, uploaded, tooLarge, refused (the server said no), abandoned (given up before it finished), refusedLocally (failed the SDK's own check) and faults. With initApp, pass pageSnapshot: { routes, serviceName } alongside keys.replay.

Node, browser, Express, Fastify, NestJS, GraphQL, and Prisma, Next.js, and React

Import and API Parameters Returns Behavior
@anectico/sdk/node initNode(options?) AnecticoOptions Promise<AnecticoClient> Starts the client and installs Node HTTP and process handlers.
registerProcessHandlers(client) client cleanup function Installs uncaught exception, rejection, and shutdown handlers. Fatal exceptions and rejections are printed to stderr before the best-effort telemetry flush. The first terminal event owns one capture, flush, and exit sequence; capture failure or secondary terminal events cannot bypass or duplicate it. Anectico owns SIGTERM/SIGINT only when no application listener is present; an application listener must close work and spans before awaiting client.stop().
setupNodePropagation(trustedUrls?) string and regular-expression destination allowlist void Idempotently installs W3C trace and baggage propagation and Node HTTP instrumentation, including synchronized CommonJS and ESM node:http/node:https exports. Preload @anectico/sdk/node/register before application imports. An application propagator registered first is preserved and should include both W3C Trace Context and W3C Baggage.
setupNodeFrameworkInstrumentation() — void Idempotently registers privacy-safe GraphQL execution and resolver and Prisma operation and query instrumentation. The Node preload calls this before application imports.
anecticoExpressMiddleware(client, options?) client; ExpressMiddlewareOptions Express handler Creates request spans and extracts trace context. Identity baggage is ignored unless explicitly trusted.
anecticoExpressErrorHandler(client) client Express error handler Captures route errors and must be registered after routes.
anecticoFastifyPlugin(instance, options) Fastify instance; AnecticoFastifyPluginOptions Promise<void> Registers global lifecycle hooks for request spans, async request identity, errors, timeouts, body aborts, and response disconnects.
@anectico/sdk/browser initBrowser(options?) BrowserOptions extends AnecticoOptions with optional browserRoutes and browserRouting (history or hash) Promise<BrowserClient> Creates a consent-managed browser client; handlers, Web Vitals and fetch and XHR tracing start only for granted categories. Declared route templates optionally attribute hard-navigation samples.
registerBrowserHandlers(client) client cleanup function Installs global error or rejection and lifecycle flush handlers. Stops on a non-persisted pagehide; canceled navigation and back and forward cache suspension leave the client running.
AnecticoProvider client, children React node Exposes the client through React context.
useAnectico() — AnecticoClient Reads the provider client; throws outside AnecticoProvider.
useSpan(name) span name { span, endSpan } Creates a component-lifetime span and ends it on unmount.
AnecticoErrorBoundary client, children, optional fallback and hooks React component Captures render errors while preserving custom fallback behavior.

ExpressMiddlewareOptions supports ignorePaths, safe recordHeaders, trustIncomingIdentity, requestIdentityHook, and spanNameHook. The identity hook resolves a server-authenticated customer for the current request, overrides untrusted inbound identity baggage, propagates the identity downstream, and rebinds the matching preload HTTP root. Exact and trailing-* ignored paths also suppress the generic incoming HTTP span installed by the Node preload. Sensitive headers are redacted even when requested.

Browser fetch and XHR, Express, Fastify, and Node HTTP spans strip URL userinfo, query strings, and fragments. Each HTTP attribute is bounded independently and marked with a .truncated=true sibling when shortened. Node telemetry uses only request scope or trusted incoming baggage for person attribution; it never assigns unscoped background work the process-global anonymous identity.

For a managed raw Node HTTP server without the Express or Fastify integration, set ignoreIncomingRequestPaths on initNode() (or the comma-separated ANECTICO_IGNORE_INCOMING_REQUEST_PATHS) to suppress health, readiness, and internal control traffic. Matching supports exact paths and a trailing *; query strings do not affect matching.

AnecticoFastifyPluginOptions requires client and supports the same ignore, header, trust, and span-name controls plus requestIdentityHook. The identity hook resolves the authenticated customer for the current request without mutating process-global SDK identity, and rebinds the matching preload HTTP root so every span in the server trace has the same customer. Its ignored paths likewise apply to both Fastify and preload HTTP spans.

Application onTimeout hooks registered after the plugin retain the request's trace, parent span, and customer identity across async cleanup even though the request span has already ended. This also applies when a socket timeout interrupts a body upload.

Fastify socket timeouts mark the framework span as an error with http.request.timed_out=true. If no response headers were sent, that span omits http.status_code; if headers were already sent, it retains their HTTP status even though the request timed out.

For NestJS Apollo and Prisma, preload @anectico/sdk/node/register before the application entry point. Named GraphQL operations and bounded resolver spans are recorded automatically; literal values are replaced with *, variables are not recorded, repeated list paths are merged, and trivial property resolvers are omitted. GraphQL execution errors promote the named operation span to error status, record the original resolver exception, and add graphql.error.count even when Apollo returns HTTP 200. Prisma operations and parameterized database queries appear as child spans, making repeated N+1 work visible.

For Next.js App Router, preload @anectico/sdk/node/register with NODE_OPTIONS before next start, then memoize one initNode() client from the root instrumentation.ts register hook. An async onRequestError hook may await captureError, but it must not attach request headers, cookies, query values, or action payloads. Add only trusted same-application or downstream origins to propagateTraceHeaderUrls; internal Route Handler calls are not trusted implicitly. Next's OpenTelemetry spans then preserve browser → Server Action → fetch → Route Handler causality and identity. Name business Actions and attach bounded safe attributes through the active OpenTelemetry span; never record FormData, payment tokens, or other server-action secrets.

LLM wrappers and source maps

API Parameters Returns Behavior
wrapOpenAI(client, anectico, options?) OpenAI client; Anectico client; { captureContent? } same OpenAI client Instruments chat.completions.create calls in place, streaming and non-streaming. Idempotent.
wrapAnthropic(client, anectico, options?) Anthropic client; Anectico client; { captureContent? } same Anthropic client Instruments messages.create calls in place, streaming and non-streaming. Idempotent.
uploadSourceMap(options) release, dist, filename, content, endpoint, API key, optional fetch and timeoutMs Promise<void> Uploads one source map with a 30-second default deadline. CI should normally use anectico symbols upload-sourcemap.
collectSourceMaps(directory, fs) directory; required filesystem facade CollectedSourceMap[] Finds .map files for a custom uploader. The packaged CLI supplies the Node filesystem adapter.

A streamed call from either wrapper is recorded as one span when the stream is exhausted, fails, or is abandoned; the stream object and its chunks or events are returned unchanged, and a stream that is never iterated is never recorded.

Stream telemetry retains the identity and trace context present when the provider call began, even if another request later consumes the stream. Non-streaming calls preserve provider promise helpers such as withResponse(). Source-map uploads reject redirects, close unused response bodies, and exclude symlinks and special files when collecting a directory.

Abandoning a stream — breaking out of the loop, or closing it early — is not an error. The span keeps its normal status and carries anectico.gen_ai.stream.abandoned = true instead, so early exits stay out of your error rate while remaining distinguishable. Any token counts on such a span are the provider's last reported values and are partial by construction. A mid-stream failure is an error, and the provider's error reaches you unchanged.

Neither wrapper changes your request. OpenAI token counts come only from the provider's final usage chunk, so request stream_options: { include_usage: true } yourself — the wrapper never adds it, because doing so inserts an extra empty-choices chunk into your loop and is rejected outright by some OpenAI-compatible gateways. Anthropic needs no such parameter. A count the provider never reported is recorded as absent, never as zero. captureContent defaults to false and can record sensitive prompts and completions.

Validation and utility exports

These root exports support framework authors and custom integrations. Normal applications should prefer init, AnecticoClient, and the documented subpath clients.

API Parameters Returns and behavior
resolveConfig(options?) partial AnecticoOptions Resolves environment values, defaults, and explicit options into ResolvedConfig. It does not validate required values.
validateConfig(config) resolved config void; throws for missing credentials and service identity, invalid URL, protocol, and rates, or inconsistent batching and timeouts.
validateAPIKey(key) string void; throws when the key does not match the supported API-key shape.
isAPIKeyFormat(value) string Boolean shape check without throwing.
maskAPIKey(key) string Log-safe prefix plus last four characters. Never use the result for authentication.
generateErrorId() — Hyphenless random error and message ID.
parseStackTrace(error, maxFrames=50) Error; limit StackFrame[] parsed from V8, Firefox, or Safari stacks.
formatStackTrace(frames) StackFrame[] OpenTelemetry stacktrace string.
framesToSerializable(frames) camel-case frames Backend wire frames with in_app.
resolveCaptureOptions(options?, defaultLevel='error') capture options or default level Capture defaults used by custom capture implementations.
new UserManager() — Mutable global error-user holder with setUser, getUser, and clearUser; values are copied.
userToAttributes(user) AnecticoUser Flat OpenTelemetry enduser.* and user.data.* string attributes.
new IdentityManager(storage?) optional Web Storage-like adapter Identity holder with getDistinctId, getAnonId, identify, group, getGroups, and reset. Also exported from @anectico/sdk/analytics, with SessionManager and memoryStorage(), for request-scoped clients.
sharedIdentity() — Process and page-wide IdentityManager shared by core signals and analytics.
contextWithDistinctId(ctx, id) OTel context and ID New context carrying request-local identity and W3C baggage.
distinctIdFromContext(ctx) OTel context Request-local ID or undefined.
adoptDistinctIdFromBaggage(ctx) trusted OTel context New context that promotes baggage identity; authenticate first.
new BreadcrumbBuffer(capacity=100) positive capacity Ring buffer with size, isEmpty, add, serialize, and clear.
createTransport(config) validated ResolvedConfig TransportComponents containing enabled OTel providers and signal handles.
flushTransport(components, timeoutMs) components and deadline Promise<void>; force-flushes all providers without stopping them and reports aggregated failures.
shutdownTransport(components, timeoutMs) components and deadline Promise<void>; shuts down all providers and reports aggregated failures.

The Node subpath additionally exports removeDistinctIdFromBaggage(ctx) for stripping an inbound Anectico identity member while retaining trace context, plus the request-context helpers listed above.

Client delivery and mobile evidence boundaries

Browser flag evaluation uses a project-bound CLIENT_DELIVERY key with literal flags:evaluate, or a customer-owned trusted backend. Never embed a definition-reading flags:read key. Browser surveys use a separate CLIENT_DELIVERY surveys:respond key, with separate product capture analytics:write authority. Provision both through preview and confirmation with fixed expiry and idempotency; agent credentials never enter the page.

The mobile native clients are described in mobile setup. A current decision is not stored exposure, an accepted survey receipt is not visible answers, and canonical identity joining does not imply equal anonymous assignments or shared sessions. The server propagates survey source sessions and refuses incomplete aliases; earlier browser or REST runs do not qualify that changed authority. Keep original IDs and read back exact evidence before claiming a pass. React Native and Flutter have typed native delivery bridges; Linux SwiftPM and Android JVM cores are not runtime qualification. Browser replay-steps evidence does not establish native replay support.