Appearance
JavaScript
The simpledatalogs package sends events, log lines and exceptions from browsers, browser extensions, React Native and Node. It is one ES module of about 9 KB gzipped, with no dependencies and with TypeScript types, and it follows every sender rule for you.
Install
sh
npm install simpledatalogssh
pnpm add simpledatalogssh
yarn add simpledatalogsNode 18 or later, any modern browser, a Manifest V3 extension or React Native. The package is ESM only, and its types ship in the package.
Early access
The package is published with the public launch. Until then, your invitation includes the package source.
Set up with one command
sh
npx simpledatalogs initIt detects Next.js, Nuxt, SvelteKit, React or Vue on Vite, React Native/Expo, plain HTML, Node and Laravel, asks for the key (or takes --key), shows a diff of the few lines it adds, writes them only after you say yes, sends a setup_test event and prints the dashboard link. Browser frameworks get the one-line autocapture; flags: --framework, --endpoint, --app-url, --dashboard-url, --dir, --yes, --no-test.
The onboarding page and the new-key dialog show this command with your key and --dashboard-url filled in, so the link it prints opens your environment's dashboard. The link is only printed; nothing is sent with it.
Track your first event
js
import { createClient } from 'simpledatalogs'
const sdl = createClient({ ingestKey: 'sdl_pub_…' })
sdl.track('signup', { plan: 'pro' })track() only queues: it returns at once and never throws. Every event gets a UUID event_id and a ts the moment you call it, so retries never count twice.
Create the client once per key, for example in a module you import everywhere:
js
// analytics.js
import { createClient } from 'simpledatalogs'
export const sdl = createClient({ ingestKey: import.meta.env.VITE_SIMPLEDATALOGS_KEY })Event options
js
const sdl = createClient({ ingestKey: 'sdl_pub_…', defaultProperties: { app_version: '1.4.0' } })
sdl.track('purchase', { order_id: 7 }, { eventId: storedUuid, ts: purchasedAt })- Default properties go into every event, log line and exception.
setDefaultProperties(props)replaces them; the event's own props win on a clash. eventIdis a UUID you keep, so re-sending the same event, after an app restart for example, is counted once. Anything that isn't a UUID gets a new id, with one warning.tssays when it happened: aDate, epoch milliseconds or ISO 8601 text from the last 10 years. Anything else becomes now, with one warning.log(level, message, props, { eventId, ts })takes the same options.
Autocapture
One option sends page views, sessions and errors, with no track() calls. It is off until you turn it on.
js
import { init } from 'simpledatalogs'
const sdl = init({ key: 'sdl_pub_…', autocapture: true })init({ key }) is the short form of createClient({ ingestKey }); both take every option below.
| Flag | With autocapture: true | What you get |
|---|---|---|
pageViews | on | page_view on load and on every SPA route change that changes the path. Props: path (never the query), title; a session's first page view adds referrer, referrer_host and utm_source … utm_content |
sessions | on | session_start, a session_id on every event, and session_end after 30 idle minutes (duration_s, page_views) |
errors | on | Uncaught errors and unhandled promise rejections as error log lines with stack, file, line and path |
outboundLinks | off | outbound_click (url, host) for links to other sites |
trackedClicks | on | A click on an element with data-track="signup" sends that event, with its data-track-* attributes as props and path |
uncaughtExceptions | on, Node only | A critical log line, then exit code 1 as Node would, after up to 2 s of flushing |
respectDoNotTrack | on | Do Not Track or Global Privacy Control turns automatic capture off; your own calls still go (see Do Not Track) |
Pick features with an object:
js
init({ key: 'sdl_pub_…', autocapture: { errors: false, outboundLinks: true } })- No cookies. The session id lives in the tab's
sessionStorage, so each tab has its own session. A closed tab sends nosession_end; its last event marks the end. - Bounded. At most 5 copies of one error message and 50 errors per page load.
- SPA routes are caught by wrapping
history.pushStateandhistory.replaceState;shutdown()restores them.
Active and background
In pages and React Native apps, every event tracked now says whether someone was using it, so the Sessions tab can tell active sessions from passive ones:
app_stateisactivewhile the page is visible or the app is in front, elsebackground.engaged_msis the time in front since the previous event.- Mark background work yourself with
track('sync_finished', { app_state: 'background' }); your value wins. activity: 'off'sends neither. Back-dated events, log lines, Node and workers never carry them.
Log lines
js
sdl.log('error', 'Payment failed', { order_id: 7 })
sdl.log('info', 'Checkout opened')
try {
await pay()
} catch (error) {
sdl.log('error', error) // stored as "TypeError: card declined"
}The level is one of debug, info, notice, warning, error or critical. An Error becomes its Name: message.
Exceptions
js
try {
await pay()
} catch (error) {
sdl.captureException(error, { props: { order_id: 7 } })
}
sdl.installGlobalHandlers() // uncaught errors and unhandled rejectionscaptureException sends an exception object with type, message, handled and stack frames, so errors group into issues by type, message and the first frame of your own code.
| Option | Meaning |
|---|---|
props | Props for this exception, on top of the default properties |
handled | true by default; false marks a crash nobody caught, stored as critical |
fingerprint | Your own grouping key, up to 128 characters |
eventId, ts | As for track() |
- Stacks from Chrome, Node, Safari, Firefox and Hermes are read into at most 50 frames. Query strings are dropped from file names;
node_modules,node:and native frames don't count as your code. - The client's
releaseoption is sent with every exception. installGlobalHandlers()reports ashandled: false, at most 5 copies of one message and 50 in all, and returns a function that removes the handlers.- Page or worker: listens for
errorandunhandledrejection; your own handlers keep working. - React Native: chains
ErrorUtils. A fatal error waits up to 1 second for the flush, then goes on to the previous handler. - Node: listens for
uncaughtException. When nothing else listens, it prints the error and exits with code 1 after up to 2 seconds, as Node would.
- Page or worker: listens for
- Autocapture's
errorsflag sends browser errors aserrorlog lines instead. Use one or the other.
Identity
Set an anonymous device id to unlock active devices and per-device pages:
js
sdl.setDeviceId(anonymousInstallId)
sdl.setUserId(accountId) // optional
sdl.setDeviceId(null) // clears itBoth apply to events tracked afterwards. You can also pass deviceId and userId to createClient. sdl.setSessionId(id) sets your own session_id the same way; a new one starts a new visit. Never use an email or a name; see Privacy for senders.
Props
Props must be strings, finite numbers or booleans:
js
sdl.track('export_finished', { format: 'png', duration_ms: 412, is_pro: true })Any other value (an object, NaN, undefined) is skipped, with one warning.
In the browser
Script tag
For a site without a bundler, one tag does everything. The tag is the opt-in, so page views, outbound clicks, data-track clicks and errors are captured:
html
<script src="https://cdn.jsdelivr.net/npm/simpledatalogs@0.5.0/dist/simpledatalogs.min.js" data-key="sdl_pub_…" defer></script>The tag runs in web mode: no cookies and no browser storage. Events wait in memory and go out when the page hides, and visitors and visits are counted on our side (see Web analytics).
| Attribute | Meaning |
|---|---|
data-key | Your public key (required) |
data-endpoint | Another ingest host, e.g. http://localhost:4001 |
data-autocapture="false" | Manual calls only |
data-outbound-links="false" | Stop tracking clicks on links to other sites |
data-storage="true" | Keep unsent events in localStorage and a tab session in sessionStorage, as the npm package does |
Your own calls use the global client: simpledatalogs.track('signup'), or no code at all:
html
<button data-track="signup" data-track-plan="pro">Start free</button>unpkg serves the same file at https://unpkg.com/simpledatalogs@0.5.0/dist/simpledatalogs.min.js.
Early access
The CDN URLs work once the package is published to npm with the public launch.
With a bundler
js
import { init } from 'simpledatalogs'
export const sdl = init({ key: import.meta.env.VITE_SIMPLEDATALOGS_KEY, autocapture: true })- Use a public key (
sdl_pub_…). The SDK warns if it sees a secret key in a web page. - Unsent events survive reloads. The outbox lives in
localStorage, so a closed tab sends its events on the next page load. - Page hide flushes. On
pagehideor a hidden tab, onefetchwithkeepalivesends up to 60 KB. Anything unconfirmed stays stored and is resent later. - Cross-origin works. Ingest answers CORS preflights, and there are no cookies.
In a browser extension
In a Manifest V3 service worker the SDK runs in worker mode, not as Node:
js
// background.js
import { init } from 'simpledatalogs'
export const sdl = init({ key: 'sdl_pub_…' })
sdl.installGlobalHandlers()- The outbox survives the worker stopping. With the
storagepermission in your manifest, it lives inchrome.storage.local; without it, in memory. - Sends every 10 seconds, since Chrome stops an idle worker after 30 seconds. Events still waiting are sent when the worker next starts.
- No
User-Agentheader, and a warning if you ship a secret key. - Popups, options pages and content scripts can share the storage with a prefix of their own:
js
import { chromeStorageAdapter, init } from 'simpledatalogs'
const sdl = init({ key: 'sdl_pub_…', storage: chromeStorageAdapter(chrome.storage.local, 'popup:') })In React Native
The SDK detects React Native and skips browser autocapture. Pass AsyncStorage to keep unsent events across launches and AppState to flush on background and run sessions; see React Native / Expo.
In Node
js
import { createClient } from 'simpledatalogs'
const sdl = createClient({ ingestKey: process.env.SIMPLEDATALOGS_KEY }) // sdl_sec_…
sdl.track('job_finished', { queue: 'emails', duration_ms: 812 })
// Before the process exits:
process.on('SIGTERM', async () => {
await sdl.shutdown()
process.exit(0)
})- Use a secret key (
sdl_sec_…) on servers. - Call
await sdl.shutdown()before the process exits. In Node the outbox is kept in memory, so this is what sends the last batch. - Batches go one at a time over one reused keep-alive connection.
Flushing
Events go out every 10 seconds (every 5 while you develop: a local site, an unpacked extension, React Native's __DEV__ or NODE_ENV=development), or at once when 50 are waiting. The first batch goes 1 second after the client starts. You rarely need to flush by hand, but you can:
js
await sdl.flush() // sends everything now; never rejects
console.log(sdl.pendingCount) // events not yet confirmed by a 202flush() resolves when the attempt ends, whether or not it succeeded. Anything undelivered stays in the outbox.
Opt-out
js
sdl.setEnabled(false) // deletes every unsent event and ignores new ones
sdl.setEnabled(true) // collects againThe SDK doesn't remember the choice. Your app's own setting is the source of truth, so apply it on every page load.
Do Not Track
By default, Do Not Track and Global Privacy Control turn off automatic capture only. Your own track(), log() and captureException() calls still go.
To honour them for every send, pass respectDoNotTrack: true:
js
const sdl = init({ key: 'sdl_pub_…', autocapture: true, respectDoNotTrack: true })With the signal on, the client starts opted out, as if you called setEnabled(false), so unsent events are deleted. Calling setEnabled(true) after your own consent prompt overrides it.
Offline and errors
The SDK handles every answer for you:
| Answer | What the SDK does |
|---|---|
202 | Removes the batch from the outbox |
401 | Keeps everything, warns once, and re-checks the key after 1 minute, then every 10 minutes |
413 | Halves the batch and resends; each 202 doubles it back to 50. A single event over 256 KB is dropped |
415 | Drops that batch, with one warning |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, never sooner than Retry-After |
Problems are logged once each with console.warn. No call ever throws into your app.
Caps
Storage is small and shared with your page or app, so the outbox is bounded:
- at most 1,000 events or 2 MB (
maxOutboxEvents,maxOutboxBytes); - only when that fills, for example after hours with a revoked key, are the oldest unsent events dropped, with a warning;
- a stored outbox older than 30 days is deleted, whatever key wrote it.
track() never writes storage itself. The outbox is saved once per turn of the event loop.
Options
js
const sdl = createClient({
ingestKey: 'sdl_pub_…', // required
endpoint: 'https://edge.simpledatalogs.com', // the ingest host
deviceId: null,
userId: null,
flushIntervalMs: 10_000,
firstFlushDelayMs: 1_000,
maxBatchEvents: 50,
maxRequestBytes: 256_000,
maxKeepaliveBytes: 60_000,
maxOutboxEvents: 1_000,
maxOutboxBytes: 2_000_000,
requestTimeoutMs: 15_000,
autocapture: false,
defaultProperties: {},
release: null,
respectDoNotTrack: false,
})| Option | Default | Meaning |
|---|---|---|
ingestKey | — | Your sdl_pub_… or sdl_sec_… key. Without one, nothing is sent |
endpoint | https://edge.simpledatalogs.com | The ingest host; /v1/ingest is added |
deviceId, userId | null | Identity on every event |
defaultProperties | {} | Props merged into every event, log line and exception |
release | null | Your app's release, sent with exceptions |
flushIntervalMs | 10000 (5000 while you develop) | How long a partial batch waits |
firstFlushDelayMs | 1000 | Wait before the first batch, so a new setup shows data at once |
maxBatchEvents | 50 | Events per request |
maxRequestBytes | 256000 | Bytes per request |
maxKeepaliveBytes | 60000 | Bytes in the page-hide request |
maxOutboxEvents, maxOutboxBytes | 1000, 2000000 | The outbox caps |
requestTimeoutMs | 15000 | One request's timeout |
autocapture | false | true, or an object of flags; see Autocapture |
respectDoNotTrack | false | Makes Do Not Track and Global Privacy Control an opt-out for every send |
storage | localStorage in browsers, chrome.storage.local in an extension worker, none elsewhere | localStorage, or any object whose getItem, setItem and removeItem may return Promises (AsyncStorage); null keeps the outbox in memory |
appState | — | React Native's AppState: flushes on background and drives sessions |
runtime | detected | browser, worker, react-native or node; sdl.runtime says which is in use |
logger | console | Where warnings go; null silences them |
sendUserAgent | true in Node only | Whether to send simpledatalogs-js/<version> as the User-Agent |
Next steps
- Events and log objects: what the SDK sends.
- Privacy for senders: what to keep out of props.
- Prompt recipes: let your coding agent add tracking for you.