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-scoped key with analytics:write. Add ingest:write only when the same SDK also sends
traces, logs, metrics, or errors.
Capture an event
JavaScript / TypeScript
import { AnalyticsClient } from '@anectico/sdk/analytics';
const events = new AnalyticsClient({
endpoint: 'https://api.anectico.com',
apiKey: process.env.ANECTICO_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_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_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. 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/client 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 and means the request never landed — this is also the answer when the upload is
cut short in transit, so a truncated or reset connection is safe to send again.
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.
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.
In React Native, delivery happens inside the native queue, so flush() there reports that the flush
attempt finished and nothing more; see the React Native reference for what its bridge does report.
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
Open Analytics → Events & trends, select the event, and pivot to a customer who triggered it. The event should also appear in that customer's timeline alongside surrounding errors, traces, and replay.
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/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/rate groups can appear in the selected window; exceeding the limit refuses the entire
quality read. source_completeness stays unknown, including for empty results.