@habeetat/sdk-browser
v0.2.2
Published
Framework-free browser client for Habeetat platform analytics (track, page views, consent)
Keywords
Readme
@habeetat/sdk-browser
Framework-free browser client for Habeetat platform analytics. Use it on pages that do not run the
React SDK — an Astro landing page, a static site — and get the same identity the app uses after
login. @habeetat/sdk-react wraps this same client.
npm install @habeetat/sdk-browserA public page (anonymous events)
<script>
import { createAnalytics } from '@habeetat/sdk-browser';
const analytics = createAnalytics({
// same-origin proxy to the platform's /sdk/ — or https://api.<your-platform-domain>
platformUrl: '',
publishableKey: 'hpk_…', // Platform Manager → app → Credentials
consent: 'pending', // wait for the cookie banner
});
document.querySelector('#cta')?.addEventListener('click', () => analytics.track('cta_clicked'));
window.addEventListener('cookie-consent', (e) => analytics.setConsent(e.detail ? 'granted' : 'denied'));
</script>The page's origin must be declared in the app's manifest
(spec.capabilities.analytics.anonymousOrigins). Anonymous events carry no user and no tenant —
only the browser's anonymous id, which the app's own events carry too after login, so the platform
links the landing history to the user without any identify call.
Another domain (a portal that sends visitors to the app)
A first-party id cannot cross domains by itself: the portal on portal.example and the app on
app.example would give one visitor two ids. The link carries it.
On the portal, add the id when a link to the app is followed, not in the static href (a copied
link would hand the id to whoever opens it):
document.querySelectorAll('a[href^="https://app.example/"]').forEach((a) =>
a.addEventListener('click', () => { a.href = analytics.linkUrl(a.href); }),
);linkUrl adds ?hid=<anonymous id> once consent is granted, and leaves the url as it is before
that. getAnonymousId() returns the id itself, or null without consent.
On the app's side the client (this one, or @habeetat/sdk-react's) reads hid when it starts. If
the value is a UUID, the client:
- removes it from the address before anything is recorded;
- adopts it only when this browser has no anonymous id yet;
- stores it only once consent is granted.
A hid that is not a UUID is left alone. Both sites send to the same app, so the portal's origin
goes in the app's anonymousOrigins.
What the client does
| | |
|---|---|
| Delivery | batches (20 events or 10 s), retries network errors / 503 / 429 with backoff, same messageIds so a retry counts once; queue capped at 1000 |
| Identity | anonymous id in localStorage['habeetat.aid']; with cookieDomain also a first-party cookie shared by subdomains; in memory if storage is blocked; from another domain through ?hid= (above) |
| Session | rotates after 30 minutes idle, emits $session_start |
| Page views | $page_view on load and every pushState / replaceState / back; autoPageViews: false to call page() yourself |
| Consent | granted (default) · pending (hold in memory, persist nothing) · denied (discard, stop); setConsent() from your banner; on granted an id the browser already stored wins over the one used while waiting |
| Routing | signed in (getAccessToken returns a token) → /sdk/v1/events; else with a key → /sdk/v1/events/anonymous; else hold until sign-in |
| Unload | delivers the queue with fetch(..., { keepalive: true }) when the page is hidden |
| Refusals | none pending denied blocked origin-not-allowed unknown-key … stop that route for the session with one console warning; never retried |
track() and page() never throw and never wait for delivery. Personal data in properties and
end-user consent are the page's responsibility.
