{"_id":"@alplus/sdk","_rev":"5-3a0abec3672267106dd2b3cc6d91aa7c","name":"@alplus/sdk","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@alplus/sdk","version":"0.1.0","license":"MIT","_id":"@alplus/sdk@0.1.0","maintainers":[{"name":"hasieratik","email":"hi@alplustech.com"}],"homepage":"https://alplus.dev","bugs":{"url":"https://github.com/alplus-dev/alplus-dev/issues"},"dist":{"shasum":"6f04a36bd48a95b56636b8593d19a4490293e289","tarball":"https://registry.npmjs.org/@alplus/sdk/-/sdk-0.1.0.tgz","fileCount":17,"integrity":"sha512-n7FsxDqGBTlOJMb/HktnPfpQKwoMAKTRdWdsCvnhRKuCustlPi8RSiDULO9NqEWCeNJ8hH4qA4swdc7rC3aAFw==","signatures":[{"sig":"MEUCIQCDH11wV0TSGO54HQgfX8hMJYEHiJ85WVhab2/WaNLEkQIgeJOjR3OD81Hdt3asoLTXLl2HXNSNHE8gzPseson+4IM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":70633},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./node":{"types":"./dist/node/index.d.ts","import":"./dist/node/index.js","require":"./dist/node/index.cjs"},"./cloudflare":{"types":"./dist/cloudflare/index.d.ts","import":"./dist/cloudflare/index.js"}},"gitHead":"a90dc45b186c45acddf5a127f4994144b14a8a76","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json --noEmit && tsup"},"_npmUser":{"name":"hasieratik","email":"hi@alplustech.com"},"repository":{"url":"git+https://github.com/alplus-dev/alplus-dev.git","type":"git","directory":"packages/sdk"},"_npmVersion":"10.9.4","description":"Official instrumentation SDK for Alplus (console.alplus.dev): Monitor heartbeat pings in v0.1, Observe error tracking and Measure custom events land in later 0.x releases.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.1.0_1785577463244_0.4663892514461798","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@alplus/sdk","version":"0.2.0","keywords":["uptime","cron","heartbeat","monitoring","cloudflare-workers","healthcheck","error-tracking","observability","analytics"],"license":"MIT","_id":"@alplus/sdk@0.2.0","maintainers":[{"name":"hasieratik","email":"hi@alplustech.com"}],"homepage":"https://alplus.dev","bugs":{"url":"https://github.com/alplus/sdk/issues"},"dist":{"shasum":"4ed5313469ea8bfb9a9145e69289903c2f1b2e7e","tarball":"https://registry.npmjs.org/@alplus/sdk/-/sdk-0.2.0.tgz","fileCount":18,"integrity":"sha512-Pklzk3tFufXgWtsvuYYofDl/XfMosUl3IpaT8Bm5SAvGcD0/e0qC8CEDMNQ/VcWHAvKJdqD1GJ3A8zBdLP9IMQ==","signatures":[{"sig":"MEUCIQD2GQEp/wyR1jHwlTCXMEXVMuJCQNmPDjdw/NLeoSG2IQIgGAMy8QaJ2wzkINTElWaFFd+cqPaJsBxjf8iO0ZQTOT8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":437176},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./node":{"types":"./dist/node/index.d.ts","import":"./dist/node/index.js","require":"./dist/node/index.cjs"},"./cloudflare":{"types":"./dist/cloudflare/index.d.ts","import":"./dist/cloudflare/index.js"}},"gitHead":"d3ddf3f6e4ae1b662709e042ef1245101257e415","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json --noEmit && tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"hasieratik","email":"hi@alplustech.com"},"repository":{"url":"git+https://github.com/alplus/sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Official instrumentation SDK for Alplus (console.alplus.dev): Monitor heartbeat pings, Observe error tracking, and Measure custom events.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.2.0_1786097146502_0.8490457664175555","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alplus/sdk","version":"0.3.0","keywords":["uptime","cron","heartbeat","monitoring","cloudflare-workers","healthcheck","error-tracking","observability","analytics"],"license":"MIT","_id":"@alplus/sdk@0.3.0","maintainers":[{"name":"hasieratik","email":"hi@alplustech.com"}],"homepage":"https://alplus.dev","bugs":{"url":"https://github.com/alplus/sdk/issues"},"dist":{"shasum":"2f0cae0f8409352c3efd0694383c32763341c57c","tarball":"https://registry.npmjs.org/@alplus/sdk/-/sdk-0.3.0.tgz","fileCount":18,"integrity":"sha512-FDL1YBhZvLZD6sTRwaycXuY+pALds5TCp8ZnjEKvjtf3AbX8kqz4heyvRkkVTV0fgzd3btvoIUn/mEIiGjSSAg==","signatures":[{"sig":"MEYCIQCncYAH6UZy8+R3mwomMh3+6LSEEWq6QXEo1urJDQkpZwIhAIe3ZAesiBUaDmjNE2oK8fnYDP/ZSxqro1fvBCJTiieZ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":618373},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./node":{"types":"./dist/node/index.d.ts","import":"./dist/node/index.js","require":"./dist/node/index.cjs"},"./cloudflare":{"types":"./dist/cloudflare/index.d.ts","import":"./dist/cloudflare/index.js"}},"gitHead":"094e03383e2ff5a54027e9da72864603bc62cf46","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json --noEmit && tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"hasieratik","email":"hi@alplustech.com"},"repository":{"url":"git+https://github.com/alplus/sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Official instrumentation SDK for Alplus (console.alplus.dev): Monitor heartbeat pings, Observe error tracking, and Measure custom events.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.3.0_1786107618582_0.04497243307426224","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alplus/sdk","version":"0.4.0","keywords":["uptime","cron","heartbeat","monitoring","cloudflare-workers","healthcheck","error-tracking","observability","analytics"],"license":"MIT","_id":"@alplus/sdk@0.4.0","maintainers":[{"name":"hasieratik","email":"hi@alplustech.com"}],"homepage":"https://alplus.dev","bugs":{"url":"https://github.com/Alplus-Tech/sdk-js/issues"},"dist":{"shasum":"2b31a9bf8466ba421d73cc0623c5133ca1781e00","tarball":"https://registry.npmjs.org/@alplus/sdk/-/sdk-0.4.0.tgz","fileCount":18,"integrity":"sha512-7f44G/OiLfms8javl/k30qEH1MHVCeMyIq61WZlMmb7+ge+S6mbehPlbmBg5ba0AWWgBvF28JhmyjYDPPmQpmw==","signatures":[{"sig":"MEYCIQC4xQwxZBRRUYVdBi1EvheodYrGC0sS40d9rd5BLVxKRAIhALGOEIF5/nOOb0bax6NhRrkt4+pCytDepbsjtrvu4D8l","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@alplus%2fsdk@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":705424},"type":"module","engines":{"node":">=18"},"exports":{".":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./node":{"types":"./dist/node/index.d.ts","import":"./dist/node/index.js","require":"./dist/node/index.cjs"},"./cloudflare":{"types":"./dist/cloudflare/index.d.ts","import":"./dist/cloudflare/index.js"}},"gitHead":"642c9e3e5eb5d6ddcee5535c1fefc09b897c1866","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json --noEmit && tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5aadce44-57b8-4245-b842-03de3328e408"}},"repository":{"url":"git+https://github.com/Alplus-Tech/sdk-js.git","type":"git"},"_npmVersion":"11.19.0","description":"Official instrumentation SDK for Alplus (alplus.dev): Monitor heartbeat pings, Observe error tracking, and Measure custom events.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.4.0_1787244134041_0.5642323547269126","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Renamed to @postdeploy/sdk"}},"time":{"created":"2026-08-01T09:44:23.052Z","modified":"2026-08-22T10:44:04.384Z","0.1.0":"2026-08-01T09:44:23.387Z","0.2.0":"2026-08-07T10:05:46.661Z","0.3.0":"2026-08-07T13:00:18.758Z","0.4.0":"2026-08-20T16:42:14.184Z"},"bugs":{"url":"https://github.com/Alplus-Tech/sdk-js/issues"},"license":"MIT","homepage":"https://alplus.dev","keywords":["uptime","cron","heartbeat","monitoring","cloudflare-workers","healthcheck","error-tracking","observability","analytics"],"repository":{"url":"git+https://github.com/Alplus-Tech/sdk-js.git","type":"git"},"description":"Official instrumentation SDK for Alplus (alplus.dev): Monitor heartbeat pings, Observe error tracking, and Measure custom events.","maintainers":[{"name":"hasieratik","email":"hi@alplustech.com"}],"readme":"# @alplus/sdk\n\nOfficial instrumentation SDK for [Alplus](https://alplus.dev), the\nCloudflare-native dev toolkit built around three pillars — **Monitor**\n(uptime and heartbeat checks), **Observe** (error tracking), and **Measure**\n(product analytics) — on one platform, one dashboard, and one bill. This\npackage ships `heartbeat()` (Monitor); `init`/`captureException`/\n`captureMessage`/`flush`/`close`, automatic global error capture,\nbreadcrumbs, and scope (`setUser`/`setTag`/`setContext`) for Observe; and\n`sendMeasureHit()` (Measure, browser-only as of 0.3.0 — see below). See\n[Roadmap](#roadmap) below for what's not here yet.\n\n## Install\n\n```sh\nnpm install @alplus/sdk\n```\n\nZero runtime dependencies. Requires Node.js >= 18 for the `/node` entry\npoint; the `/cloudflare` and neutral (`.`) entry points work anywhere\n`fetch` and `URL` are global (workerd, browsers, modern runtimes). Do not\nimport `./core`. That entry is adapter-internal.\n\n## Monitor: `heartbeat()`\n\n`heartbeat(token, options?)` sends a ping to a **Heartbeat monitor** you've\ncreated at [alplus.dev/dashboard](https://alplus.dev/dashboard). Heartbeat\nmonitors watch scheduled jobs — cron tasks, nightly batches, queue\nworkers — that are supposed to run on a schedule; Alplus alerts you when a\nping doesn't show up on time or reports failure. Every ping ultimately hits:\n\n```\nGET|POST https://ingest.alplus.dev/h/{token}\n```\n\nso the SDK is just a small, resilient, typed wrapper around a single HTTP\nrequest — nothing you couldn't do with `curl`, which is the point: your job\ndoesn't need a network SDK at all if a shell one-liner already works.\n\n### Quickstart\n\nCreate a Heartbeat monitor in the console first and copy its token (looks\nlike `hb_...`). Every example below pings that monitor.\n\n#### Cloudflare Workers (Cron Triggers)\n\nUse `ctx.waitUntil()` so the ping doesn't block (or get cancelled when) the\nscheduled handler returns:\n\n```ts\nimport { heartbeat } from \"@alplus/sdk/cloudflare\";\n\nexport default {\n  async scheduled(event, env, ctx) {\n    ctx.waitUntil(heartbeat(\"hb_your_token\", { state: \"start\" }));\n    await runScheduledTask();\n    ctx.waitUntil(heartbeat(\"hb_your_token\", { state: \"finish\" }));\n  },\n};\n```\n\n#### Node.js (cron job or standalone script)\n\n```ts\nimport { heartbeat } from \"@alplus/sdk/node\";\n\ntry {\n  await runNightlyJob();\n  await heartbeat(\"hb_your_token\", { state: \"finish\" });\n} catch (err) {\n  await heartbeat(\"hb_your_token\", {\n    state: \"fail\",\n    message: err instanceof Error ? err.message : String(err),\n  });\n}\n```\n\nOr skip the try/catch and use the process exit-code shortcut, which is\nhandy at the tail of a script:\n\n```ts\nprocess.on(\"exit\", (code) => {\n  void heartbeat(\"hb_your_token\", { exitCode: code });\n});\n```\n\n#### Plain shell / crontab (no SDK required)\n\nHeartbeat monitors are just a URL, so anything that can run `curl` in a\ncrontab works without installing this package. The exit-code path form\n(`/h/{token}/{exitCode}`) maps `0` to a finish ping and any other value\n(`1`-`255`) to a fail ping — the same semantics as the SDK's `exitCode`\noption:\n\n```sh\n# crontab -e\n0 2 * * * /usr/local/bin/nightly-backup.sh; curl -fsS \"https://ingest.alplus.dev/h/hb_your_token/$?\" > /dev/null\n```\n\n### `heartbeat()` options reference\n\n```ts\nimport { heartbeat } from \"@alplus/sdk/node\"; // or /cloudflare, or \"@alplus/sdk\"\n\nawait heartbeat(token, options?);\n```\n\n| Option | Type | Default | Semantics |\n| --- | --- | --- | --- |\n| `state` | `\"start\" \\| \"finish\" \\| \"fail\"` | _(none — a plain ping)_ | `start` records the beginning of a run so the console can track its duration. `finish` closes it out as a success. `fail` **opens an incident immediately** on the monitor. Mutually exclusive with `exitCode`. |\n| `exitCode` | `number` | _(none)_ | Shortcut for `state`: `0` maps to `finish`, any value `1`-`255` maps to `fail`. Mutually exclusive with `state`. |\n| `message` | `string` | _(none)_ | Diagnostic text attached to `fail` pings, shown on the incident in the console. Silently truncated to 2048 characters. |\n| `pingId` | `string` | a fresh generated id | Idempotency key, reused across retries of one call. |\n| `baseUrl` | `string` | `https://ingest.alplus.dev` | Override the ingest origin. |\n| `fetchImpl` | `typeof fetch` | the platform's global `fetch` | Inject a custom `fetch` implementation — primarily for unit tests. |\n| `debug` | `boolean` | `false` | Log a `console.warn` when retries are exhausted or an internal error occurs. |\n\n`heartbeat()` **never throws or rejects**, regardless of network failure or\nan internal SDK bug — every ping is attempted up to 3 times total with\njittered exponential backoff, and failures are swallowed after retries are\nexhausted (set `debug: true` to log them instead).\n\n## Observe: error tracking\n\n```ts\nimport { init, captureException, captureMessage, flush, close } from \"@alplus/sdk/node\"; // or \".\" or \"/cloudflare\"\n\ninit({ key: \"alp_p_your_ingest_key\", environment: \"production\", release: \"1.4.2\" });\n// That's it -- uncaught exceptions and unhandled rejections are captured\n// automatically from here on (browser and Node; see below for Cloudflare).\n\ntry {\n  riskyOperation();\n} catch (err) {\n  captureException(err, { context: { feature: \"checkout\" } });\n}\n\ncaptureMessage(\"payment webhook received an unexpected status\", \"warning\");\n\n// Before a short-lived script exits:\nawait flush(2000);\n```\n\n`init(options)` configures a single module-scope client (call it once per\nprocess/isolate; calling it again reinitializes rather than throwing).\n`key` must be a project API key with the `ingest` scope. `captureUnhandled`\n(default `true` on browser/Node) controls automatic capture — see below.\n\n`captureException(error, options?)` accepts any thrown value — an `Error`,\na string, or anything else JavaScript allows you to `throw`. A non-`Error`\nvalue is normalized into a synthetic error with the original value\npreserved under `contexts.extra.non_error_value`. `options.context` is\nmerged into `contexts.extra`; `options.user`/`.tags`/`.contexts`/\n`.breadcrumbs` are per-capture scope overrides (see\n[Scope](#scope-setuser-settag-setcontext) below), and `options.mechanism`\noverrides the default `\"generic\"` (the automatic capture paths set their\nown). Returns the client-generated `err_`-prefixed event id synchronously,\nso you can surface it to a user (\"reference id `err_...`\") even before the\nevent is sent. The same error object captured twice within ~2 seconds (for\nexample by both the automatic handler and a manual call) is deduplicated to\none event and returns the same id both times.\n\n`captureMessage(message, level?, options?)` records a non-exception event;\n`level` defaults to `\"info\"` and must be one of `\"fatal\" | \"error\" |\n\"warning\" | \"info\"`. `options` accepts the same scope overrides as\n`captureException`.\n\n`flush(timeoutMs?)` (default 2000ms) forces an immediate send and resolves\n`true` if it drained in time. `close(timeoutMs?)` detaches automatic\ncapture/breadcrumb instrumentation, flushes, and then makes further capture\ncalls no-ops for the rest of the process.\n\nEvery capture/transport path is wrapped so **the SDK never throws into your\napplication** — a malformed capture, a network failure, or an internal bug\nis caught, optionally logged via `debug: true`, and swallowed. Automatic\ncapture never changes your program's own behavior either: the browser still\nlogs an uncaught error to the console exactly as it would with no SDK\ninstalled, and a wrapped Cloudflare handler still re-throws so the Worker's\nown error response still happens.\n\n### Automatic global error capture\n\nInstalling the SDK captures errors by default — opt out with\n`captureUnhandled: false`, not opt in.\n\n- **Browser** (`.`): `window.addEventListener(\"error\"\n  /\"unhandledrejection\")`, attached on `init`, detached on `close()`.\n- **Node** (`/node`): `process.on(\"uncaughtException\"/\"unhandledRejection\")`.\n  `uncaughtException` captures, flushes (bounded to ~2s so a slow ingest\n  endpoint can't hang shutdown), and then calls `process.exit(1)` itself —\n  Node suppresses its own default crash-and-exit behavior the instant any\n  listener is attached, so this is the only way to reproduce it, not an\n  SDK choice to be more aggressive than Node's default. `unhandledRejection`\n  captures and flushes but deliberately does **not** exit the process, since\n  Node's own default there is configurable (`--unhandled-rejections`) and\n  forcing an exit would change the behavior of an app that set it to\n  `warn`/`none` on purpose.\n- **Cloudflare** (`/cloudflare`): no process-global hooks exist in workerd,\n  so there is no `captureUnhandled` flag to flip here. Wrap your handler(s)\n  instead:\n\n  ```ts\n  import { init, wrapHandler, wrapScheduled } from \"@alplus/sdk/cloudflare\";\n\n  export default {\n    fetch: wrapHandler(async (request, env, ctx) => {\n      init({ key: env.ALPLUS_KEY, environment: \"production\" });\n      // application code; a thrown error is captured, flushed via\n      // ctx.waitUntil, and re-thrown -- the Worker's own error response\n      // still happens exactly as it would with no SDK installed.\n      return handleRequest(request);\n    }),\n    scheduled: wrapScheduled(async (controller, env, ctx) => {\n      init({ key: env.ALPLUS_KEY, environment: \"production\" });\n      await runScheduledTask();\n    }),\n  };\n  ```\n\nEvery captured event — automatic or manual — carries `mechanism`:\n`\"onerror\"`, `\"onunhandledrejection\"`, `\"uncaughtException\"`,\n`\"unhandledRejection\"`, `\"instrumentation\"` (Cloudflare's wrappers), or\n`\"generic\"` (a direct `captureException`/`captureMessage` call).\n\n### Breadcrumbs\n\n```ts\nimport { addBreadcrumb } from \"@alplus/sdk\"; // or \"/node\"\n\naddBreadcrumb({ category: \"checkout\", message: \"clicked pay\", level: \"info\" });\n```\n\nA ring buffer (default 30 entries, `maxBreadcrumbs` on `init`) attached to\nevery subsequent captured event, giving a trail of what led up to it. Never\nrecords input values or request/response bodies, and strips query strings\nfrom URLs by default — and `data` on any breadcrumb (manual or automatic) is\nscrubbed of `password`/`secret`/`token`/`api_key`-shaped keys the same way\n`context`/`setContext` payloads are.\n\n- **Browser** (`.`): automatic, on by default — navigation\n  (`pushState`/`replaceState`/`popstate`), delegated clicks (a CSS selector\n  only, e.g. `button#submit.btn-primary`, **never** element text), patched\n  `console.log`/`.warn`/`.error`, and patched `fetch` (method, URL with the\n  query string stripped, status, duration). Every patch is reversible on\n  `close()`, composes with a `fetch` your own code already patched (wraps\n  whatever `fetch` currently is, not a reference saved at import time), and\n  a breadcrumb-recording failure never affects the underlying call.\n- **Node** (`/node`): manual `addBreadcrumb` only, scoped the same way\n  `setUser`/etc are — see [Scope](#scope-setuser-settag-setcontext). No\n  automatic `fetch` breadcrumbs yet (a global `fetch` patch writing\n  anywhere other than a request-scoped buffer would reintroduce the same\n  cross-request attribution bug scope avoids).\n- **Cloudflare** (`/cloudflare`): no ambient breadcrumb buffer at all — pass\n  `breadcrumbs: [...]` directly in `captureException`/`captureMessage`'s\n  options for a one-off capture.\n\n### Scope: `setUser`/`setTag`/`setContext`\n\n```ts\nimport { setUser, setTag, setContext } from \"@alplus/sdk\"; // or \"/node\"\n\nsetUser({ id: \"user_123\", email: \"jane@example.com\" }); // or null to clear\nsetTag(\"plan\", \"agency\");\nsetContext(\"cart\", { items: 3 });\n```\n\nMerged into every subsequent captured event until changed or cleared.\n**How this is scoped differs sharply by platform, and the difference is\ndeliberate** — a naive module-global `setUser` is safe in a browser tab (one\nuser, no concurrent requests) and a real bug on a server (request A's\n`setUser` would still be set during request B the instant they overlap):\n\n- **Browser** (`.`): a single module-global scope. Correct here.\n- **Node** (`/node`): backed by `AsyncLocalStorage`, active **only** inside\n  `withScope(fn)`:\n\n  ```ts\n  import { withScope, setUser, captureException } from \"@alplus/sdk/node\";\n\n  async function handleRequest(req: Request) {\n    return withScope(async () => {\n      setUser({ id: req.userId });\n      try {\n        return await process(req);\n      } catch (err) {\n        captureException(err); // attributed to req.userId, not some other\n        // concurrent request's user\n        throw err;\n      }\n    });\n  }\n  ```\n\n  Calling `setUser`/`setTag`/`setContext`/`addBreadcrumb` **outside** an\n  active `withScope` is a no-op (logged in debug mode) — never a\n  module-global write. A single-threaded script can wrap once around its\n  whole body; a request-handling server should wrap per request.\n- **Cloudflare** (`/cloudflare`): no ambient scope API at all — a Workers\n  isolate can serve concurrent requests, and this package cannot assume the\n  `nodejs_compat` flag `AsyncLocalStorage` needs there. Pass\n  `user`/`tags`/`contexts` directly in `captureException`/\n  `captureMessage`'s options instead; this is always safe regardless of\n  isolate concurrency, and it works as an escape hatch on every platform,\n  where an explicit per-capture value overrides the ambient one.\n\n### Batching and per-platform flush behavior\n\nCaptured events are queued in memory and sent as a batch once any of these\nis first true: 10 events queued, ~64 KB of estimated serialized size, or 5\nseconds since the oldest queued event. Free-text fields (`message`,\nexception `value`, stack traces, context objects) are capped at the same\nboundary the server enforces, so an oversized payload is trimmed by the SDK\nrather than discovered by a server rejection.\n\n- **Browser** (`.`): the 5-second idle timer applies, plus a `pagehide`\n  listener that best-effort flushes any remaining queue via\n  `fetch(..., { keepalive: true })` when the page is closed or bfcache'd.\n  (This uses `fetch` keepalive rather than `navigator.sendBeacon`, because\n  `sendBeacon` cannot carry the `Authorization` header this endpoint\n  requires.)\n- **Node** (`/node`): the 5-second idle timer applies, and is `unref()`'d so\n  it never keeps a short-lived script running on its own — call `flush()` or\n  `close()` before your script exits, or a queued batch waiting on the timer\n  can be lost.\n- **Cloudflare Workers** (`/cloudflare`): the idle timer is always disabled\n  (a Workers isolate can be evicted between requests, so a background timer\n  is not a reliable flush mechanism there). Call `ctx.waitUntil(flush())` at\n  the end of every request that captured something:\n\n  ```ts\n  import { Hono } from \"hono\";\n  import { init, captureException, flush } from \"@alplus/sdk/cloudflare\";\n\n  const app = new Hono<{ Bindings: { ALPLUS_KEY: string } }>();\n  app.use(\"*\", async (c, next) => {\n    init({ key: c.env.ALPLUS_KEY, environment: \"production\" });\n    await next();\n  });\n  app.onError((err, c) => {\n    captureException(err, { context: { path: c.req.path } });\n    c.executionCtx.waitUntil(flush());\n    return c.text(\"Internal Server Error\", 500);\n  });\n  ```\n\n  The 10-event/64KB thresholds still trigger an immediate flush regardless\n  of platform.\n\n### What Observe does not do yet\n\nNo `sampleRate`/`beforeSend`, no `tunnel` proxy option, no browser offline\nqueue, no `XMLHttpRequest` breadcrumbs, no automatic Node/Cloudflare `fetch`\nbreadcrumbs (manual `addBreadcrumb` covers Node; Cloudflare has no ambient\nbreadcrumb buffer at all — pass `breadcrumbs` explicitly per capture), no\nframework helpers (Hono/Express/React), and no source map upload tooling.\nSee [Roadmap](#roadmap).\n\n## Measure: `sendMeasureHit()`\n\nAvailable from the browser entry point (`@alplus/sdk`) only as of 0.3.0 —\n**not** from `/node` or `/cloudflare`. `POST /m`'s only auth is a real\nbrowser's own `Origin` header, which neither a Node nor a Workers `fetch`\ncall ever carries, so calling this from either of those adapters always\nsilently recorded nothing; the exports were removed rather than left as a\nworking-looking trap (`docs/sdk/02-dx-improvements.md` section 5). Use the\nfirst-party `/m.js` browser tracker for anything server-side.\n\n```ts\nimport { sendMeasureHit } from \"@alplus/sdk\"; // browser only\n\nawait sendMeasureHit({\n  site: \"proj_your_project_id\",\n  url: \"https://shop.example.com/checkout/complete\",\n  type: \"custom_event\",\n  name: \"signup_completed\",\n});\n```\n\nThis is a low-level, programmatic wrapper around `POST /m` for use\nsomewhere a `<script>` tag isn't an option in a real browser page — an SPA\nroute change, a dynamically-injected form submit handler. It is **not** a\nreplacement for Alplus's first-party browser tracker script; a real website\nshould load that script instead.\n\n`POST /m` has no API key: the only gate is the request's `Origin` header,\nchecked against your project's allowlisted domains. A real browser attaches\nthat header automatically on every POST and JavaScript cannot override it,\nwhich is what makes it trustworthy as a control — and is exactly why this\nfunction is browser-only as of 0.3.0 (see above). The response is always\n`204 No Content` whether the hit was recorded or rejected, by design, so\nthere is nothing in the response to tell the difference if your domain\nisn't on the project's allowlist.\n\nThis function deliberately does **not** accept an `origin` override to work\naround that. Fabricating an Origin value to get a call past the allowlist\nwould defeat the one security control this endpoint has.\n\n| Option | Type | Notes |\n| --- | --- | --- |\n| `site` | `string` | The project id (`proj_...`) — public, not secret. |\n| `url` | `string` | Full page URL the hit is recorded for. |\n| `referrer` | `string \\| null` | Optional; omitted or `null` for a direct hit. |\n| `type` | `\"pageview\" \\| \"custom_event\"` | Defaults to `\"pageview\"`. |\n| `name` | `string` | Required when `type` is `\"custom_event\"`. |\n| `props` | `Record<string, unknown>` | Accepted for forward compatibility; the server discards it before rollup and never stores it. |\n| `baseUrl` / `fetchImpl` / `debug` | — | Same as `heartbeat()`'s options above. |\n\nThere is no retry: unlike Observe, a hit carries no client-generated\nidempotency id, so retrying risks double-counting a visit rather than\nrecovering a lost one. Never throws.\n\n## Troubleshooting\n\n- **Heartbeat returns 404** — the token is wrong, or the monitor has been\n  deleted or paused in the console. Copy the token again from the\n  monitor's detail page.\n- **Pings/hits are fire-and-forget** — none of these functions return\n  monitor state, issue status, or analytics numbers. Check the relevant\n  page in the console.\n- **Nothing shows up in the console** — confirm the process actually\n  reaches the network (egress rules, offline dev environment, VPN) and\n  that `baseUrl` wasn't overridden to point somewhere else. For Observe,\n  confirm the key carries the `ingest` scope. For Measure, see the Origin\n  caveat above — this is the single most common \"it silently does\n  nothing\" cause.\n\n## Roadmap\n\nNot available in `@alplus/sdk@0.3.x` — no stub exports, no\nreserved-but-throwing placeholders. If it isn't documented above, it\ndoesn't exist in this package yet:\n\n- `beforeSend`, `sampleRate`, and the `tunnel` proxy option.\n- Framework helpers: `@alplus/sdk/hono`, `/express`, `/react`.\n- Automatic outbound-`fetch` breadcrumbs on Node/Cloudflare, and any ambient\n  breadcrumb/scope API on Cloudflare at all (pass `breadcrumbs`/`user`/\n  `tags`/`contexts` explicitly per capture there instead).\n- `XMLHttpRequest` breadcrumbs (browser `fetch` breadcrumbs ship; XHR does\n  not).\n- A browser offline queue.\n- Source map upload tooling (`alplus-cli sourcemaps upload`).\n- An IIFE/UMD browser build for non-bundler `<script>` tag usage.\n- **`./core`** is not a host import. Use `.`, `./node`, or `./cloudflare`.\n- **Ruby / Rails** — `alplus-ruby` is the first-party gem. It is not this\n  npm package. Hex and RubyGems first publish is separate from this 0.3.x\n  line.\n\n## Versioning\n\nThis package is `0.x`: minor versions (`0.2` -> `0.3`) may introduce\nbreaking changes as new modules land, though `heartbeat()`, `init`,\n`captureException`, `captureMessage`, `flush`, and `close` documented here\nare expected to stay stable going forward — 0.3.0's additions\n(automatic capture, breadcrumbs, scope) are all additive on top of that\nsurface. `sendMeasureHit()` is no longer exported from `/node`/`/cloudflare`\nas of 0.3.0 (see [Measure](#measure-sendmeasurehit) above) — a breaking\nchange made while the package was still unpublished, so it cost nothing.\nPatch versions are always backwards compatible. See\n[GitHub releases](https://github.com/Alplus-Tech/sdk-js/releases) for the\nchangelog.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}