Skip to content
Console
Browse documentation
Guide

Capture product events

Measure application milestones, declare sampling, and inspect capture quality.

On this page

Product events record meaningful application milestones such as checkout_submitted, payment_failed, or agent_step_completed.

Use them to measure workflows and connect customer behavior with errors, traces and replay. Capture is explicit; broad automatic clickstream collection is not enabled.

Use a project-bound browser_capture, mobile_capture or server_capture key with only analytics:write. If the SDK also sends traces, logs, metrics or errors, configure a different native_ingest key with only ingest:write. See native intake keys.

Capture an event

JavaScript / TypeScript

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

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

events.capture('checkout_submitted', {
  cart_size: 3,
  payment_provider: 'stripe',
});

Python

import os
import anectico

events = anectico.AnalyticsClient(
    endpoint='https://api.anectico.com',
    api_key=os.environ['ANECTICO_CAPTURE_API_KEY'],
)
events.capture('checkout_submitted', {
    'cart_size': 3,
    'payment_provider': 'stripe',
}, distinct_id='user_8842')

Go

events, err := anectico.NewAnalytics(anectico.AnalyticsConfig{
	Endpoint: "https://api.anectico.com",
	APIKey:   os.Getenv("ANECTICO_CAPTURE_API_KEY"),
})
if err != nil {
	return err
}
defer func() { _ = events.Stop(ctx) }()

err = events.Capture(ctx, "checkout_submitted", map[string]any{
	"cart_size":        3,
	"payment_provider": "stripe",
}, anectico.WithDistinctID("user_8842"))

Swift (iOS)

Anectico.capture("checkout_submitted", properties: [
    "cart_size": 3,
    "payment_provider": "stripe",
])

Kotlin (Android)

Anectico.capture(
    "checkout_submitted",
    mapOf("cart_size" to 3, "payment_provider" to "stripe"),
)

React Native

await Anectico.capture('checkout_submitted', {
  cart_size: 3,
  payment_provider: 'stripe',
});

Flutter

await Anectico.capture('checkout_submitted', properties: {
  'cart_size': 3,
  'payment_provider': 'stripe',
});

Stop the event client during graceful shutdown so buffered events can finish sending. In JavaScript, the event client shares the active identity and browser session with the main SDK, unless you pass it an identity and a session of its own: a Node.js server builds one such client per request, as request-scoped clients shows. Python and Go can use the shared identity or an explicit request-scoped ID as shown above. Go's batch-oriented Analytics.Identify queues a person merge without changing the process-global identity, so one server can synchronize multiple people without attributing later background telemetry to the last person. Use client.Identify only for the stateful single-user flow. Do not create a second customer ID field inside event properties.

If you call POST /api/v1/capture directly, send exactly one JSON envelope containing the events array. Concatenated JSON documents are malformed; the entire request is rejected and no prefix of the batch is accepted.

When to retry

If you call POST /api/v1/capture directly, retry on 429 and on any 5xx, with backoff. A 503 carries Retry-After. Treat a timeout, unreadable acknowledgement or interrupted connection as uncertain; preserve the original envelope and message IDs for a bounded retry.

Follow the validated acknowledgement's retry instruction for each original position. An explicit invalid-input refusal requires a correction; an oversized request must be split. An unrecognized response remains uncertain at any HTTP status. Preserve original IDs, timestamps and payloads when retrying uncertain positions, and do not resend accepted siblings. See capture acknowledgements.

Use a stable message_id for each logical occurrence. An identical retry in the same project within seven days is recognized as a retry and does not consume product allocation twice. If the original request omitted timestamp, a retry that also omits it reuses the original server-assigned time. Reusing an ID with a different normalized payload or explicit timestamp refuses that event alone with invalid_event; the rest of the batch is decided normally. Send a new ID only for a new occurrence. Normal event-age and validation limits still apply. Without a message ID, repeated requests are separate admissions.

An organization whose allocations still need to be chosen receives 503 dependency_unavailable for the whole request (retryable). New product events beyond the product threshold receive per-position quota_exceeded. See product capture limits.

Know when events were dropped

capture() is non-blocking: it queues the event and returns before anything is sent, so it can never tell you whether delivery worked. Three surfaces do.

flush() tells you whether everything queued was accepted. It reports success only when every attempted event received a validated queue acknowledgement, or the queue was empty. HTTP 2xx alone is insufficient, and queue acceptance does not prove query visibility. When explicit refusals were dropped it reports failure by value — false in JavaScript and Python, an error wrapping ErrAnalyticsDropped in Go. JavaScript and Python raise when uncertain events remain queued; Go reports ErrAnalyticsRetryPending. A mixed outcome can contain both drops and uncertain events; inspect the counters and callbacks as well. Go errors can match both ErrAnalyticsDropped and ErrAnalyticsRetryPending.

A delivery handler tells you the moment a batch is dropped. Pass onDeliveryError / on_delivery_error / OnDeliveryError when you construct the client and it is called once per dropped group of positions with the response status, the number dropped, and a stable lower-case reason code such as unauthorized or payload_too_large. Retryable failures never call it — those events are still queued. Per-item refusals can arrive in an HTTP 200 ledger; a drop count is not proof that the corresponding user action never reached storage through an earlier attempt.

A counter tells you the running total. stats() / Stats() reports recorded, delivered (events the server accepted), dropped, and queued. A non-zero dropped is worth alerting on: the most common cause is a revoked or wrong-project API key, which rejects every batch with 401 or 403 while the rest of your application carries on unaware.

Each drop is also written to the SDK's log at WARN, naming the status and the count.

On mobile, flush() returns per-signal outcomes for productEvents, nullable logs and traces, including rejected, pending and unknown counts. Inspect the mobile SDK delivery reference for the result fields; read stored events independently before concluding delivery.

Choose useful events

Record events that help an engineer answer a production question:

  • a workflow entered a meaningful state;
  • a rollout or provider choice changed behavior;
  • a customer-visible operation succeeded or failed; or
  • an agent or background job completed a significant step.

Avoid high-volume noise such as every mouse movement, DOM mutation, or internal function call.

Verify

To prove that the exact events of a controlled test arrived under the right person, have your agent follow Plan and prove product events.

Ask your agent which people triggered the event (list_event_triggerers over MCP, or anectico events triggerers) and to read one of their timelines (get_person_timeline). The event should appear in that customer's timeline alongside surrounding errors, traces, and replay. The timeline result carries a link that opens the customer as a read-only proof page.

Declare product-event sampling

If your application or collector samples product events before capture, attach its declaration to each submitted event's properties. Existing SDK property maps accept this object:

{
  "environment": "production",
  "$anectico_product_sampling": {
    "source": "checkout-producer",
    "rate": 0.25
  }
}

The reserved object must contain exactly source and rate. source is a non-sensitive identifier of 1–64 ASCII characters: start with a letter or digit, then use letters, digits, periods, underscores, colons or hyphens. Use a producer label, never a person identifier or credential. rate must be a finite JSON number greater than zero and at most one; strings, null, extra fields and out-of-range values are refused as INVALID_EVENT. This property applies only to product events; identify and $groupidentify cannot carry it. Other valid items in the same batch still follow their own acknowledgements.

A rate below one is declared_sampled; one is declared_unsampled. An absent object is unknown. The declaration is retained with the stored event and its frozen source evidence. It reports what the client says; it does not identify an authorized source, prove that all upstream actions were captured, or change collection behavior. Keep it unchanged when retrying an uncertain event. Analytics measures observed events and does not scale funnel or retention results by this rate. Overall source completeness remains unknown even when every submitted event declares rate one.

This product-event declaration is separate from temporary diagnostic trace sampling grants. It does not measure events withheld by consent, missing instrumentation or upstream drops.

Inspect capture-attempt quality

After sending events, inspect the project's retained attempt evidence with the CLI:

anectico --project <project-id> analytics quality \
  --start-time 2026-09-01T00:00:00Z --end-time 2026-09-02T00:00:00Z \
  --environment production

For MCP, discover the get_capture_quality read action, then call:

{"name":"execute_read_action","arguments":{"action":"get_capture_quality","arguments":{"project_id":"<project-id>","start_time":"2026-09-01T00:00:00Z","end_time":"2026-09-02T00:00:00Z","environment":"production"}}}

Both require analytics:read; MCP additionally requires mcp:read. Capture-only analytics:write cannot inspect attempts. Product and identity-control counts retain their pending, queued, uncertain and not-attempted states; invalid and quota-refused positions are reported separately. Counts include submitted retries and use coarse UTC-hour overlap. Invalid positions cover all retained project attempts because their time and environment cannot be trusted. Queued is not consumed, and zero counts do not establish completeness. Check SDK delivery callbacks and query actual events to investigate uncertainty; do not automatically resend a whole batch based on these counts. See the response contract for limits, errors and the meaning of the digest.

The same response includes sampling_provenance: "declared", sampling_declarations grouped by source and rate, and sampling_unknown for product positions without a declaration. Each group separates publication states from quota refusals. These counts include retries; two submissions with the same message ID can appear in different declaration groups while storage retains the original event. Identity-control and invalid-input counts remain separate. At most 100 distinct source and rate groups can appear in the selected window; exceeding the limit refuses the entire quality read. source_completeness stays unknown, including for empty results.