OtisDocs

SDK Reference

Identity and properties

User and group identity, properties, and reading identity in the browser.

The SDK captures who did what: user identity and group membership. Identity flows through to every span and event automatically once set. All functions below work in both server and browser runtimes.

For session and chat IDs (sessionId, chatId) and auth-provider pass-through, see Sessions.

identifyUser

Link the current session to a user and optionally record group memberships:

import { identifyUser } from "@runotis/sdk";

identifyUser("user-123", { company: "acme", team: "eng" });

A user can belong to at most one group per group type, and at most 10 group types per call. The SDK throws if you pass more than 10.

In the browser, identifyUser also auto-captures session properties (user agent, language, UTM parameters). See Browser and consent for the exact keys.

setUserProperties

Set persistent user-level properties for cohort segmentation:

import { setUserProperties } from "@runotis/sdk";

setUserProperties("user-123", { plan: "pro", role: "developer" });

Feature flag exposures

Use the flag. prefix to record flag assignments:

setUserProperties("user-123", {
  "flag.dark_mode": "variant_b",
  "flag.new_onboarding": "control",
});

If you use a feature-flag platform like LaunchDarkly, Statsig, PostHog, GrowthBook, Unleash, or Hypertune, the Feature flags guide shows how to wire them through OpenFeature so every flag evaluation auto-records the assignment — no manual setUserProperties calls at each evaluation site.

Flag-property capture binds to the identified user. Anonymous sessions (no identifyUser or targetingKey set) are skipped: the assignment has no user to attach to.

setGroupProperties

Set persistent properties on a group (company, team, workspace):

import { setGroupProperties } from "@runotis/sdk";

setGroupProperties("company", "acme", { plan: "enterprise", size: 500 });

Revenue amounts

If you want Otis to weight retention, churn and expansion by revenue rather than by account count, assert the amount at the same call site that asserts plan. No extra API and no extra call:

setGroupProperties("account", account.id, {
  plan: "pro",
  mrr_minor: 49900,  // integer minor units (cents), monthly
  currency: "USD",   // ISO-4217, three letters
  seats: 12,         // optional — seats you provision, not seats in use
});

mrr_minor is in minor units and must be an integer. 49900 is $499.00. Sending 499.99 is read as $5.00, not as $499.99 — it is a valid number, so nothing rejects it. If your billing system gives you a decimal, multiply and round before you send it.

Three conventions, each of which changes the answer if you skip it:

  • Normalize to a month. Divide an annual contract down yourself. Otis will not amortize for you, because proration is a question your billing system can answer and Otis cannot.
  • Send currency whenever you send mrr_minor. Amounts without one cannot be combined.
  • seats means seats you provisioned. Otis already counts the users it observes in the group; that number falls when someone stops using the product, which is not a change in what you bill. Send the entitlement if you want it, and leave it out if you don't have one to send.

To compare groups of accounts by size — "do accounts over $10k/mo behave differently?" — send a band as a string alongside the number:

setGroupProperties("account", account.id, {
  mrr_minor: 249900,       // $2,499/month
  currency: "USD",
  mrr_band: "1k-5k",       // a label, not a number — and on the same scale
});

Don't rely on a raw mrr_minor as a cohort. If your prices land on a handful of distinct values, Otis will treat those values as categories — which produces comparisons between individual price points rather than between meaningful tiers. Choose the bucket boundaries yourself and send them as labels.

Update these on the same schedule you update plan — on plan change, or on a periodic refresh. Otis keeps the history; each call only needs to state what is true now.

setSessionProperties

Set properties on the current session. Use this for facts about one visit rather than about the user in general, such as how the visit started or which layout your app showed:

import { setSessionProperties } from "@runotis/sdk";

setSessionProperties({ entry_point: "shared_link", layout: "compact" });

The call doesn't need an identified user, so you can set properties on an anonymous visitor's session too.

Which session the properties attach to

The SDK uses the first session it finds, in this order:

  1. If you pass a sessionId in the second argument, the SDK uses it.
  2. Otherwise, in the browser, it uses the session the SDK manages for you. That is the __otis_session cookie, or an in-memory session before the user gives consent (see Browser and consent).
  3. Otherwise, it uses the sessionId from an enclosing withContext.

If the SDK finds none of these, the call does nothing, because there is no session to attach the properties to. On a server outside withContext, pass the session yourself:

setSessionProperties({ entry_point: "api" }, { sessionId: auth.sessionId });

Pass the same session ID you use for that visit everywhere else. A different ID records the properties against a different session. Sessions explains how to choose one.

Reserved session keys

In the browser, identifyUser already records some properties on the session. setSessionProperties throws an error if you pass one of these keys, because the two calls would overwrite each other's value every time identifyUser runs:

  • any key that starts with _
  • utm_source, utm_medium and utm_campaign
  • device_type, browser, os, language and country

Choose a different name instead, such as store_country rather than country.

setDocumentProperties

Set properties on a document — whatever your users build, whether you call it a project, a page, a deck or a report. Use this for what your product knows about the document that Otis cannot work out from the activity inside it:

import { setDocumentProperties } from "@runotis/sdk";

otis.withContext({ documentId: doc.id }, () => {
  setDocumentProperties({ type: "Landing page", template: "hero_split" });
});

type decides how the document is grouped

One key is special. type tells Otis what kind of thing the document is, and it is what analysis groups documents by — "which page kinds convert best", "where do campaign pages get stuck".

Otis works this out on its own when you don't declare it, from the artifact events inside the document and from the document's name. Declaring type replaces all of that: nothing Otis infers outranks what you say. Use the words your product already uses, and keep them consistent between documents. If a value doesn't match the kinds you've told Otis your product builds, it is still recorded — the mismatch is worth seeing, because usually it means the list needs updating.

Every other key is an ordinary property you can segment on, like user properties.

Which document the properties attach to

The SDK uses the first document it finds, in this order:

  1. If you pass a documentId in the second argument, the SDK uses it.
  2. Otherwise, it uses the documentId from an enclosing withContext.

There is no automatic document the way there is an automatic session, so if neither is present the call does nothing. Outside withContext, pass the document yourself:

setDocumentProperties({ type: "PDP" }, { documentId: doc.id });

Pass the same document ID you use for that document everywhere else, or the properties are recorded against a different document.

Document IDs are stored as you send them, and are not hashed the way user, session and artifact IDs are. Send an ID, not a title or a file path — anything you put here is stored in full.

Naming a document

Don't use the key name. It is on the list of property keys Otis drops as personal data, which applies to every property call and cannot tell a document's name from a person's. Use title if you want to record one.

You usually don't need to: Otis already takes a document's name from your rename events.

Property keys and values

The key format and merge rules below apply to all property calls: setUserProperties, setGroupProperties, setSessionProperties, setDocumentProperties, and the group types passed to identifyUser. Set-once properties apply to user properties only.

Key format

Property keys, group types, and group IDs are trimmed and lowercased before use, and must match [a-z0-9_.-]+. The SDK throws on invalid input.

ExampleResult
"plan"✅ plan
"Plan"✅ normalized to plan
"first-name"✅ hyphens allowed
"flag.dark_mode"✅ dots allowed (used for flag. prefix)
"company name"❌ throws; spaces not allowed
"role!"❌ throws; ! not in allowed set

Because keys are lowercased, "Plan" and "plan" are the same key. Keep key spelling stable across call sites. Inconsistent casing or underscore/hyphen mixing won't produce separate keys; it'll just churn the value on every write.

Per-key merge semantics

Each call sends only the keys you pass; it does not replace the full property set for the entity. Setting properties incrementally works as you'd expect:

setUserProperties("user-123", { plan: "pro" });
// ...later, somewhere else...
setUserProperties("user-123", { role: "admin" });
// Both `plan` and `role` are now set on user-123.

Each key is stored independently. Within a single key, the most recent write wins, unless the key is set-once (see below).

Set-once properties

Some user properties describe how a user started, such as how they signed up or which campaign brought them in. A later value for one of these is a different fact rather than a correction, so Otis keeps the first value it receives for each user and ignores later writes to the same key.

Five keys work this way for every project, with no setup:

  • utm_source, utm_medium and utm_campaign
  • signup_source
  • first_seen_date

To make your own keys set-once, list them in setOnceKeys when you call initOtis:

initOtis({ apiKey, serviceName: "web", setOnceKeys: ["signup_channel"] });

setUserProperties("user-123", { signup_channel: "partner_referral" });
// A later write to signup_channel for user-123 is ignored.

Your list adds to the five built-in keys and can't turn any of them off. Spell each key the way you spell it in setUserProperties. You can list up to 64 keys; initOtis throws if you pass more.

setOnceKeys applies only to calls from the app that declares it. If a second app, or a service that sends telemetry without the Otis SDK, writes the same key without declaring it, that write replaces the stored value. Declare the key in every app that writes it.

Three practical points:

  • Name keys after what they represent. Use present-tense names (plan, role, team_size) for things that change, and don't list them in setOnceKeys. Use initial-state names (first_plan, signup_channel, original_referrer) for the keys you do list.
  • Only send a value you have. An empty string doesn't count as a first value, so it neither locks the key nor clears a stored value. Leave the key out of the call when you don't know it.
  • If a write appears not to take effect, the key may be set-once. Writing a new value to a set-once key from your application has no effect.

Reading identity in the browser

In a Next.js client component, the useOtis() hook exposes the current userId, sessionId, and isAnonymous, plus identifyUser and sendFeedbackSignal. Values are reactive — components re-render when identifyUser() is called or the session rotates. See Next.js for the full hook reference.

On this page