Pulse SDK — product analytics for your app
@orkestia/lumen-pulse is the browser client for Pulse, Lumen's product analytics. It sends page views and the events you name from any web UI (React, Vue, Svelte, plain HTML) to Lumen, waits for consent before it sends or stores anything, and links a visitor's anonymous events to their user id when they sign in. It has no runtime dependencies.
The source is public under the MIT license at github.com/orkestia/lumen-pulse.
v0.2.0). The API may change before 1.0. The package is not on the npm registry: install it from GitHub as shown below.Before you start
- Lumen is enabled for your organization (Enable). Without it every request fails with
403 PULSE_NOT_PROVISIONED. - You have a browser Pulse key. Create it in Pulse → Setup, in the Lumen app or in the Cockpit app. Enter the website addresses your app runs on and one project name. The key starts with
lump_and is shown once.
A browser Pulse key can only write product events. It cannot read anything, and it only works from the addresses and for the project it was created with, so it is safe to ship in a web page. Never put a lumk_ key in a browser: Pulse rejects it with WRONG_KEY_SCOPE, and it can write logs to your organization.
| Address you enter | Accepts |
|---|---|
example.com or https://example.com | Exactly https://example.com |
https://*.example.com | Any subdomain of example.com, not example.com itself |
http://localhost:5173 | Local development. http:// is only accepted for localhost, 127.0.0.1 and ::1 |
*, https://*, https://*.com.br | Refused: they would match other people's sites |
The scheme and port must match exactly. Add each address your app is served from, including www. if you use it.
Install
npm i github:orkestia/lumen-pulse#v0.2.0
npm builds the package on install. If your npm blocks install scripts, install the built release file instead:
npm i https://github.com/orkestia/lumen-pulse/releases/download/v0.2.0/orkestia-lumen-pulse-0.2.0.tgz
Pin the tag (#v0.2.0) so a later release does not change your build. The package is ES modules only.
Initialize
import { createLumenPulse } from "@orkestia/lumen-pulse"
const pulse = createLumenPulse({
writeKey: "lump_…", // browser Pulse key
project: "my-app", // the project the key was created for
ingestUrl: "https://lumen-api.orkestia.dev",
autocapture: { pageViews: "history" }, // optional: page views, including SPA route changes
})
// When the visitor accepts analytics in your cookie banner:
pulse.setConsent("granted")
pulse.track("signup_started", { plan: "pro" })
// After sign-in or sign-up:
pulse.identify(user.id)
// After sign-out:
pulse.reset()
Create one client per page load and reuse it. writeKey, project and ingestUrl are required; createLumenPulse throws without them.
Consent
Nothing is sent, and nothing except the consent choice is stored, until consent is granted.
| Consent | Events | Storage |
|---|---|---|
unknown (default) | Dropped | Anonymous id and session id kept in memory only |
granted | Sent | Anonymous id and session id saved in localStorage |
denied | Dropped, and the unsent queue is cleared | Anonymous id and session id removed |
The choice is saved under lumen_pulse.consent, so a returning visitor who accepted keeps sending without asking again. Pass storage: null to keep everything in memory.
Events tracked before consent are dropped, not queued. That includes the first page_view from autocapture, which fires when the client is created. If the visitor accepts on that page, send it yourself:
pulse.setConsent("granted")
pulse.track("page_view")
Track events
| Method | What it does |
|---|---|
track(name, properties?, context?) | Queue an event. properties is your own JSON; context overrides fields of the automatic context |
identify(userId, traits?) | Set the user id on every later event and send an identity.alias event, which links the visitor's earlier anonymous events to that user |
reset() | Clear the user id and start a new anonymous id and session. Call it on sign-out |
setConsent(status) | "granted", "denied" or "unknown" |
flush() | Send queued events now (returns a promise) |
shutdown() | Flush, then stop the timer and remove the page listeners |
getAnonymousId(), getSessionId() | Read the current ids |
Page views. autocapture: { pageViews: true } sends one page_view when the client is created. autocapture: { pageViews: "history" } also sends one on every pushState, replaceState and back/forward navigation that changes the path, so single-page apps need no router hook. Without autocapture, call track("page_view") yourself. Page views, entry pages and bounce rate in Pulse come from page_view events.
Goals and funnels use the event names you send, so pick stable names such as signup_completed or checkout_paid (Pulse in the Cockpit app).
User ids. Use your own stable internal id, not an email address. identify traits are sent as the properties of the identity.alias event, so the same naming rules apply (see Privacy). A string org_id trait (your own account or tenant id) is also attached to the events that follow.
Sessions. In v0.2.0 the session id does not expire after inactivity: once consent is granted it stays the same in that browser until you call reset() or consent is withdrawn. Pulse counts one visit per session id.
The SDK does not capture clicks, scrolling or errors on its own. The Heatmaps views need click and scroll_depth events, which you would send with track.
Without a bundler
The package ships a script-tag entry, dist/auto.js. Host the whole dist/ folder from your site (auto.js loads a file next to it), then:
<script
type="module"
src="/assets/lumen-pulse/auto.js"
data-write-key="lump_…"
data-project="my-landing"
data-ingest-url="https://lumen-api.orkestia.dev"
data-autocapture="history"
></script>
<script type="module">
// From your cookie banner:
window.LumenPulse?.client?.setConsent("granted")
window.LumenPulse?.client?.track("cta_clicked", { cta: "hero" })
</script>
| Attribute | Meaning |
|---|---|
data-write-key, data-project, data-ingest-url | Required. Without all three no client is created |
data-consent | Initial consent: granted, denied or unknown (default). A stored choice wins |
data-autocapture | true: one page_view on load. history: also on SPA navigation |
data-include-query-params | Comma-separated query parameters allowed into the page path, such as utm_*,ref |
window.LumenPulse.client is the client; window.LumenPulse.create makes another one.
Configuration
| Option | Default | Purpose |
|---|---|---|
writeKey | required | The lump_… browser Pulse key |
project | required | The project the key was created for. Any other project fails with PULSE_PROJECT_FORBIDDEN |
ingestUrl | required | https://lumen-api.orkestia.dev |
consent | "unknown" | Initial consent when none is stored |
autocapture | off | { pageViews: true } or { pageViews: "history" } |
includeQueryParams | none | Query parameters allowed into the page path: exact names, or a prefix ending in * |
appVersion | none | Sent as context.app_version |
flushIntervalMs | 2000 | How often the queue is sent |
maxBatchSize | 20 | Events per request, capped at 50 |
storage | localStorage | Where ids are saved after consent; null keeps them in memory |
onError | none | Called when a batch is dropped; never throws into your app |
What is sent
Each event carries event_name, project, a generated event_id, occurred_at, anonymous_id, user_id (after identify), session_id, your properties, and a context with:
| Field | Value |
|---|---|
path | location.pathname, without the query string unless you allow parameters with includeQueryParams |
referrer | The referring URL without its query string or fragment |
utm | utm_* parameters of the current URL, always sent here |
locale | The browser language |
user_agent_class | desktop, mobile, bot or unknown, not the user agent itself |
consent | The consent state |
app_version | When you set appVersion |
Lumen takes your organization from the key; an organization_uuid in the event body is ignored. Product events are kept for up to 395 days, or for your organization's custom retention period when one is set. They never become logs, error groups or alerts.
Privacy
- Lumen refuses events whose property names look like personal data:
email,e-mail,password,passwd,phone,telephone,cpf,ssn,credit_card,card_number,authorization,access_token,refresh_token(any letter case). The whole request is refused with400 PULSE_PII_PROPERTY_FORBIDDEN, so one bad property drops every event in that batch. - Only names are checked, not values. Keep personal data out of property values, event names and user ids yourself.
- The query string and the referrer's query string are left out by default, because they often carry tokens and personal data.
Delivery and errors
The queue is sent every 2 seconds, as soon as a batch fills, and when the page is hidden or closed. Requests go to POST https://lumen-api.orkestia.dev/api/product/ingest/batch with the key in the X-Api-Key header, using fetch with keepalive so they finish after the tab closes. Bodies stay under 64 KB; larger queues are split.
A network error, a 5xx or a 429 is retried once: after the Retry-After the server sends (a wait longer than 10 seconds drops the batch), or after 0.5 to 1.5 seconds. Every other error drops the batch. A dropped batch calls onError with a LumenPulseError that has status (0 for a network error), code and dropped (the number of events lost).
import { createLumenPulse, LumenPulseError } from "@orkestia/lumen-pulse"
const pulse = createLumenPulse({
writeKey: "lump_…",
project: "my-app",
ingestUrl: "https://lumen-api.orkestia.dev",
onError(err) {
if (err instanceof LumenPulseError && err.code === "PULSE_ORIGIN_FORBIDDEN") {
console.warn("This address is not on the Pulse key")
}
},
})
| Code | HTTP | Meaning | SDK |
|---|---|---|---|
PULSE_AUTH_REQUIRED | 401 | No key sent | Drops |
PULSE_AUTH_INVALID | 401 | Key unknown, revoked or malformed | Drops |
WRONG_KEY_SCOPE | 401 | Not a Pulse key, for example a lumk_ key | Drops |
PULSE_ORIGIN_FORBIDDEN | 403 | The page's address is not on the key | Drops |
PULSE_PROJECT_FORBIDDEN | 403 | project is not the key's project | Drops |
PULSE_NOT_PROVISIONED | 403 | Lumen or Pulse is not enabled for the organization | Drops |
PULSE_PII_PROPERTY_FORBIDDEN | 400 | A property name looks like personal data | Drops |
PULSE_BATCH_TOO_LARGE | 413 | More than 50 events in one request | Drops (the SDK never sends more) |
PULSE_RATE_LIMITED | 429 | Organization events per minute exceeded | Retries once after Retry-After |
PULSE_QUOTA_EXCEEDED | 429 | Monthly event allowance used up | Drops |
Limits
| Plan | Events / minute | Events / month |
|---|---|---|
| Free | 1,200 | 1,000,000 |
| Pro | 10,000 | 25,000,000 |
| Enterprise | 60,000 | 250,000,000 |
Both limits apply to the whole organization, across all Pulse keys and projects, and are separate from the log and metric limits. A browser key sends at most 50 events per request.
Content Security Policy
If your site sends a CSP header, allow the ingest host:
connect-src 'self' https://lumen-api.orkestia.dev
When you host auto.js on your own site, script-src 'self' covers it.
Check it works
Open your app, accept analytics, and click around. Pulse → Events shows each event with its properties and context; in the Cockpit app, Pulse → Setup also shows the last event received and its project. If nothing arrives, add an onError that logs err.code and compare it with the table above.
Next
Auth SDK — Sign in with Orkestia
Browser PKCE / OAuth SDK for your app's end-users — hosted login, RS256 JWTs, silent renew, no client secret
App Data
Platform-owned application data — declare tables, never hold a DSN in the frontend. Workflows, Data API, PostgREST, and an admitted SQL console for operators.
