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.
Browser collection consent and Web Vitals
Consent-first default since 0.2.0.
initBrowser()collects nothing until your application grants a category. Upgrading from 0.1.x without callingsetConsent(or passing a controller created with your existing grants) stops all browser telemetry. Browser clients also refuseopenTelemetryMode: 'existing'. The packageCHANGELOG.mdlists 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,textareaorselectelements, nor fromcontenteditablecontent. Text is never taken from inside an editable region, password or hidden fields, fields withautocompleteofcc-*orone-time-code, or elements whosenameoridsuggests 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,srcand 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 classanectico-blockorrr-block) to an element: that element and everything inside it produces no event. Adddata-anectico-mask(oranectico-maskorrr-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 localmaskSelectorsandblockSelectorsto 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
mintomaxwith 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 usedir="auto", animation stops underprefers-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}).
Consent and what is sent
- While the
productgrant 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,
environmentwhen 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 theX-API-Keyheader. - 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.