Send events from your server
The backend SDK for Node sends your server's events and errors to Sequential with a server key. They go to the same person as the browser events, and into the browser session when a browser request caused them.
On this page
What it is for
Some facts are known only to your server: a payment that went through, an email that was sent, a job that failed. The browser cannot see them. The backend SDK sends them to Sequential.
- The person's page shows them in Backend activity.
- A server event that a browser request caused is in that browser session too.
- An error from your server, such as a 5xx that your API answered, can open a finding.
What it needs
- Node 18 or later. The package has no dependencies. It supports CommonJS and ES modules, and it includes types.
- A server key for the project. Owners and admins create server keys.
- The user ids that your browser code sends with
identify(). See Identify your users. - For the browser session: the Sequential SDK (sequential.js) on your pages. Only that SDK writes the context cookie. With another browser SDK, send the user id only.
Create a server key
You must be an owner or admin.
- Go to Settings → Projects → your project → Sources & keys.
- In the card Server keys, click Create a server key.
- In Label, type a name for the key, for example "API on production". Use 1 to 80 characters.
- Click Create key.
- If your account has two-factor authentication (2FA), Sequential asks for a code. Enter it.
- Copy the key. Sequential shows it one time only: "This key is shown once. Store it in your secret manager."
- Keep the key in your secret store, for example as
SEQUENTIAL_SERVER_KEY. Then click Done.
The key starts with sqs_. It is not the project's write key. Sequential refuses a server key in a request from a browser.
Server keys: exact behaviour
- A server key is
sqs_and 40 letters or digits. Sequential stores only a SHA-256 hash of it and its last 4 characters. So it cannot show the key again. - The list shows each key by its label and as
sqs_…with the last 4 characters. It also shows "Created <day> · Last used <time ago>", or "Last used never". - A project can have at most 10 active server keys. At 10, the card says "A project has at most 10 active server keys. Revoke one to create another."
- A trial project cannot have server keys. The card says "A trial project cannot have server keys. Claim the project first."
- Members see the list. They cannot create or revoke keys. Viewers do not see the tab Sources & keys.
- The 2FA step: Sequential asks for a code from your authenticator app, or a backup code, when your account has 2FA. It does not ask if you entered a code in this sign-in in the last 15 minutes.
- The Audit log records each new key and each revoked key, with its label and last 4 characters. It never records the key.
- The ingest key list of the project never shows a server key.
Rotate or revoke a key
To rotate a key, create a new key, deploy it, then revoke the old one. Sequential does not revoke a key on its own.
To revoke a key, click Revoke next to it. The card asks: "Revoke this key? Servers that send with it are refused at once." Click Revoke key. From then on, Sequential refuses requests with that key with 401. If a Sequential server misses the revoke message, it refuses the key at most 60 seconds later.
Install the SDK
The package is @usesequential/node on npm, under the MIT licence. Its source code is on GitHub: usesequential/sequential-node.
npm install @usesequential/nodeSend an event
This example is for Express. track() returns at once. The SDK sends the events in batches in the background.
import { SequentialServer, contextFromRequest } from '@usesequential/node';
const sequential = new SequentialServer({ secretKey: process.env.SEQUENTIAL_SERVER_KEY! });
app.post('/checkout', async (req, res) => {
const order = await charge(req.body);
sequential.track({
event: 'order charged',
userId: req.user?.id, // the id that your browser code sends with identify()
context: contextFromRequest(req), // the browser session of this request
props: { plan: order.plan, amount_eur: order.amount },
});
res.json(order);
});
process.on('SIGTERM', async () => {
await sequential.shutdown(); // sends the events that are still in the queue
process.exit(0);
});eventis the event name: 1 to 200 characters. It must not start with$. Use the naming rules of Send custom events.userIdis the id that your browser code sends withidentify().accountIdis the id that it sends withaccount().propsare flat properties: ids, enums and counts. Sequential stores an object or an array as JSON text.tsis the time of the event. If you do not give it, the SDK uses the current time.track()never throws. It returnsfalsewhen it drops the event, andstatscounts the reason.
Serverless functions
A function can stop as soon as it answers. Call await sequential.flush() before it returns. If you do not, the events in the queue can be lost. The SDK's timer never keeps a process alive.
How Sequential finds the person and the session
| You send | The event goes to |
|---|---|
userId and context |
That user, in that browser session |
userId only, for example from a scheduled job or a queue worker |
That user, in no session |
context only: the visitor did not sign in |
That visitor, in that session. When the visitor signs in, the event is on the user. |
| Neither | Nothing. The SDK does not send the event. |
When you send a userId and a context, Sequential keeps the session only when the context belongs to that user. A visitor can change their own cookie. So Sequential drops a context that belongs to a different visitor. The event then stays on the user, in no session, and stats.context_dropped counts it.
The person and the session: exact behaviour
- The context belongs to the user when its visitor id is the user id. It also belongs to the user when that visitor called
identify()with the user id. Sequential can check this only after it has stored thatidentify(), which can take about a minute. Until then, the event keeps the user but not the session, andstats.context_droppedcounts it. Afteridentify(), the cookie keeps the anonymous visitor id until the next page load. - A context without a
userIdcannot be checked. Sequential accepts it and counts it instats.context_unverified. This is not a refusal. The refused data in Settings does not show it. - Sequential stores the key that it used as the event's user id. The person views find the person from it when they read, as for browser events. See Identify your users.
- A server event never identifies a person. Only the browser's
identify()links a visitor to a user. - Each server event has the property
$context, which Sequential sets:user_session,user,user_context_droppedorsession_unverified. Sequential drops a property named$contextthat you send.
Where the context comes from
While the Sequential SDK records on your pages, it writes the cookie __sq_ctx on your own domain. The cookie goes to your backend with each request from your pages. contextFromRequest(req) reads it. It first reads the header x-sequential-context, and uses the cookie only when the header has no context.
- Your API is on a different site than your pages. The cookie does not go to a different site. Send the header from your page.
- Your API is on a different subdomain, for example pages on
www.shop.comand the API onapi.shop.com. Send the header, or adddata-context-cookie-domain="shop.com"to the Sequential SDK's script tag. See Sequential SDK. - A native app calls your API. Send the header.
contextFromRequest() accepts a Node request (Express, ctx.req in Koa, request.raw in Fastify, plain http) and a Fetch API Request (Next.js route handlers, c.req.raw in Hono). It returns null when the request has no context.
The context cookie and header: exact behaviour
- The value is
v1;d=<visitor id>;s=<session id>. Each id is URI-encoded. The header holds this value. The cookie holds it URI-encoded one more time, because a cookie value cannot hold;. - The value has at most 600 characters: the browser SDK does not write a longer one, and it clears the cookie.
contextFromRequest()reads only ids of at most 200 characters, and returnsnullotherwise. - The cookie has
Path=/andSameSite=Lax. It hasSecureon an https page. It does not haveHttpOnly, so your page can read it. - It lasts 30 minutes, the time without activity after which a session ends.
- Without
data-context-cookie-domain, the cookie goes only to the host of the page. The attribute accepts only letters, digits,.and-. For any other value, the SDK writes a host-only cookie and writes this error to the browser console: "data-context-cookie-domain is not a domain; the context cookie is host-only." - To send the header from your page, read the cookie, decode its value one time with
decodeURIComponent, and send the result. For an API on a different origin, your API must allow the headerx-sequential-contextin its CORS answer. - Each tab has its own session, but all tabs share the cookie. So the tab that wrote last wins.
- After
identify(), the cookie keeps the anonymous visitor id until the next page load. After a reload, it holds the stored id, which is the user id. If your app uses emails as user ids, the cookie then holds an email.
Send an error
An error from your server can open a finding, as a browser error can. The finding shows Server.
// An exception that your code caught, for example a failed job or a failed payment.
try {
await sendInvoice(order);
} catch (err) {
sequential.captureError(err, { userId: order.userId, props: { job: 'invoice' } });
throw err;
}
// A 5xx that your API answered. Send the route template, never the path with its ids.
app.use((req, res, next) => {
res.on('finish', () => {
sequential.captureHttpError({
status: res.statusCode, // the SDK sends only 500-599
method: req.method,
route: req.route?.path ?? 'unknown',
userId: req.user?.id,
context: contextFromRequest(req),
});
});
next();
});captureError()sends the error's name as its type, its message, and the first 20 lines of its stack.captureHttpError()sends only a status from 500 to 599. The type isHTTP <status>, for exampleHTTP 502. The message is the method and the route, for examplePOST /orders/:id.- Send the route template, such as
/orders/:id. Ids in the path make one problem into many findings. - Sequential stores your error messages as you send them. Keep personal data out of them.
Errors: exact behaviour
- The SDK sends an error as the event
$exception, with$exception_type,$exception_messageand$exception_stack. The message has at most 2,000 characters. The stack has at most 8,000. - A value that is not an
Errorbecomes anErrorwith that text, or with "Non-Error value thrown". captureHttpError()without a route sends(unknown route). For any status out of 500-599, it sends nothing and returnsfalse.- An error from your server opens a finding from its first occurrence. Its fix brief says "Opened from its first occurrence: an error from your server opens a finding at once."
- An error without a browser session counts in the people and occurrences of its finding, not in its sessions. The sample sessions of a finding are browser sessions only. A finding with only such errors shows "Sample sessions aren't available for this finding."
- When all the occurrences of a finding came from your server, Evidence shows Page: "From your server". Sequential does not measure releases for such a finding yet. Its fix brief says "Sequential does not verify findings from your server yet." Check the fix in the logs of your server.
Options
| Option | Default | What it does |
|---|---|---|
secretKey |
Required | The server key, sqs_… |
host |
https://api.usesequential.com |
The Sequential API. The SDK sends to <host>/i/v1/server. |
flushAt |
100, at most 500 | The SDK sends a batch when this many events are in the queue. |
flushIntervalMs |
1000 | The SDK sends the queue at least this often, in milliseconds. |
maxBatchBytes |
512 kB, at most 1 MB | The largest request. The SDK drops one event that is larger. |
maxQueue |
10,000 | The most events in memory. The SDK drops new events above it. |
maxRetries |
3 | The retries after a 429, a 5xx or a network error |
disabled |
false |
Send nothing, for example in tests. track() still checks and counts. |
onError |
None | The SDK calls it with a SequentialError for each drop: reason, events and status. |
fetch |
The global fetch |
Your own fetch |
A number option that is not a finite number, for example Number(process.env.UNSET), takes its default.
Delivery: exact behaviour
new SequentialServer()throws an error when the key is notsqs_and 40 letters or digits. The error message does not show the value. It also throws when there is nofetch: use Node 18 or later, or givefetch.- On a 429, the SDK waits for the
Retry-Aftertime, at most 60 seconds. Then it sends the batch again. WithoutRetry-After, it waits as for a 5xx. - On a 5xx or a network error, it waits about 200 ms, then about 400 ms, and so on, at most about 30 seconds. It sends the batch again at most
maxRetriestimes, so 4 times in all with the default. A retry after a 429 counts too. - On any other 4xx, it drops the batch. This includes a revoked key (401) and a billing block (402).
- Every event has a
uuid. The SDK sends the same batch again on a retry, so Sequential stores it one time. A retry that arrives in a new week is stored again. Weeks start on Monday at 00:00 UTC. track()makes the JSON of the event at once. If a property cannot be JSON, for example aBigIntor an object that refers to itself, the SDK drops the event. A later change to the property object does not change the event in the queue.- An id (
userId,accountIdor an id of the context) of more than 200 characters drops the event. statscounts:queued,sent,batches,retries,http429,dropped_invalid,dropped_no_identity,dropped_queue_full,dropped_too_large,dropped_client_error,dropped_after_retries,context_droppedandcontext_unverified.- The drop reasons in
onErrorareinvalid_event,no_identity,queue_full,event_too_large,client_errorandretries_exhausted. A throw inonErrordoes not reach your code. - The SDK sends
authorization: Bearer <key>and the user agentsequential-node/<version>.
What server events count as
- A server event has the class Custom. A server error has the class Error. See Events.
- A server event is not a page view. It never makes a session, a user or a visitor. A person with only server events is not in People and not in the user counts.
- Your usage counts sessions, so server events do not use your sessions. See Usage and limits.
- The event rate limit of the project counts them: 20,000 events a minute for all the SDKs of the project together.
- Funnels, flows and retention count them as events of the person who did them.
- Agents:
get_user_journeygivesbackend_events, the names of the server events of that person.list_errorsgives each error group asource:browser,serverorboth.
On the person's page
The person's page shows Backend activity: "Events your server sent with the backend SDK."
- Each row shows the event name, how long ago it happened, and in session when the event is in a browser session. in session opens the replay.
- The rows are in the date range of the page, newest first. The card shows at most 20, and "Showing 20 of N" when there are more.
- The card shows only names, never property values.
- If the range has no server event, the card does not show.
Privacy
- Server events store no browser, device, IP address, country or URL. Those would be your server's, shown as the user's.
- Send ids, enums and counts as properties. Do not send names, emails or text that people typed.
- Some property names mean the text of a page element, in any letter case: for example
text,form,selector,element,element_text,aria_label, and names that start with$el_or$element. In such a property, Sequential stores an email, a card number, a JWT or a Bearer or Basic credential as[redacted]. This is the same rule as for element text from the browser. Error messages are not in this rule. See Privacy at a glance. - The cookie
__sq_ctxis an analytics cookie. List it with your analytics cookies in your consent banner. See Privacy at a glance.
Answers from Sequential
The SDK handles these answers. You see them in onError and stats.
| Answer | When |
|---|---|
| 200 | Sequential accepted the request. The body gives events_ingested, context_dropped, context_unverified, dropped_no_identity and dropped_invalid. |
400 server_key_in_browser |
The request has an Origin header. |
400 invalid_body |
The body has no events list. |
401 invalid_server_key |
The key is not sqs_ and 40 letters or digits, for example the browser write key. |
401 unknown_ingest_key |
Sequential does not know the key, or the key is revoked. |
402 billing_limit_reached or billing_required |
Billing blocks the organisation. The SDK drops the batch. |
403 trial_origin_locked |
The project is a trial project. |
413 too_many_events |
The request has more than 500 events. A request also cannot be more than 1 MB. |
429 rate_limited |
More than 50 requests a second with one key, or more than the project's event rate limit. The answer has Retry-After. |
503 overloaded or auth_unavailable |
Sequential cannot take the request now. The SDK tries again. |
- Sequential drops an event with a name that is not valid, or with an id of more than 200 characters. It counts it in
dropped_invalid. Of the names that start with$, it accepts only$exception. - The list of allowed origins of the project does not apply to server events.
- Sequential does not refuse server HTTP clients, such as node-fetch, axios or curl, on this route.
- If the store that counts the 50 requests a second is unavailable, Sequential accepts the request.
Not in this version
- Node is the only backend SDK.
- A server event cannot identify a person.