Orkestia
Blog
SDKs

Pulse SDK — product analytics for your app

@orkestia/lumen-pulse: install, initialize, consent, track, identify, the browser Pulse key, privacy defaults, delivery, limits and error codes

@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.

Early access (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

  1. Lumen is enabled for your organization (Enable). Without it every request fails with 403 PULSE_NOT_PROVISIONED.
  2. 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 enterAccepts
example.com or https://example.comExactly https://example.com
https://*.example.comAny subdomain of example.com, not example.com itself
http://localhost:5173Local development. http:// is only accepted for localhost, 127.0.0.1 and ::1
*, https://*, https://*.com.brRefused: 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.

Nothing is sent, and nothing except the consent choice is stored, until consent is granted.

ConsentEventsStorage
unknown (default)DroppedAnonymous id and session id kept in memory only
grantedSentAnonymous id and session id saved in localStorage
deniedDropped, and the unsent queue is clearedAnonymous 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

MethodWhat 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>
AttributeMeaning
data-write-key, data-project, data-ingest-urlRequired. Without all three no client is created
data-consentInitial consent: granted, denied or unknown (default). A stored choice wins
data-autocapturetrue: one page_view on load. history: also on SPA navigation
data-include-query-paramsComma-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

OptionDefaultPurpose
writeKeyrequiredThe lump_… browser Pulse key
projectrequiredThe project the key was created for. Any other project fails with PULSE_PROJECT_FORBIDDEN
ingestUrlrequiredhttps://lumen-api.orkestia.dev
consent"unknown"Initial consent when none is stored
autocaptureoff{ pageViews: true } or { pageViews: "history" }
includeQueryParamsnoneQuery parameters allowed into the page path: exact names, or a prefix ending in *
appVersionnoneSent as context.app_version
flushIntervalMs2000How often the queue is sent
maxBatchSize20Events per request, capped at 50
storagelocalStorageWhere ids are saved after consent; null keeps them in memory
onErrornoneCalled 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:

FieldValue
pathlocation.pathname, without the query string unless you allow parameters with includeQueryParams
referrerThe referring URL without its query string or fragment
utmutm_* parameters of the current URL, always sent here
localeThe browser language
user_agent_classdesktop, mobile, bot or unknown, not the user agent itself
consentThe consent state
app_versionWhen 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 with 400 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")
    }
  },
})
CodeHTTPMeaningSDK
PULSE_AUTH_REQUIRED401No key sentDrops
PULSE_AUTH_INVALID401Key unknown, revoked or malformedDrops
WRONG_KEY_SCOPE401Not a Pulse key, for example a lumk_ keyDrops
PULSE_ORIGIN_FORBIDDEN403The page's address is not on the keyDrops
PULSE_PROJECT_FORBIDDEN403project is not the key's projectDrops
PULSE_NOT_PROVISIONED403Lumen or Pulse is not enabled for the organizationDrops
PULSE_PII_PROPERTY_FORBIDDEN400A property name looks like personal dataDrops
PULSE_BATCH_TOO_LARGE413More than 50 events in one requestDrops (the SDK never sends more)
PULSE_RATE_LIMITED429Organization events per minute exceededRetries once after Retry-After
PULSE_QUOTA_EXCEEDED429Monthly event allowance used upDrops

Limits

PlanEvents / minuteEvents / month
Free1,2001,000,000
Pro10,00025,000,000
Enterprise60,000250,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

Pulse in the Cockpit app

Overview, events, funnels, paths, retention and key setup.

Send data

The HTTP contract behind the SDK, for servers and other languages.

Enable Lumen

Turn Lumen on for your organization and pick a plan.