{"_id":"@abdoseadaa/raqib","_rev":"6-8436215018c8449be914049e188ea311","name":"@abdoseadaa/raqib","dist-tags":{"latest":"0.3.0"},"versions":{"0.0.1":{"name":"@abdoseadaa/raqib","version":"0.0.1","keywords":["tracker","analytics","events","sdk"],"license":"MIT","_id":"@abdoseadaa/raqib@0.0.1","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"dist":{"shasum":"bed444adcfb272aeb186140aa2577b65390b9a91","tarball":"https://registry.npmjs.org/@abdoseadaa/raqib/-/raqib-0.0.1.tgz","fileCount":62,"integrity":"sha512-1HgATx9KxMG4bfIP4S9S1k5BFGfwj5DnOoYqbYd8MGDELVEjoejflp4pbtAAFtPwcyVV7Y6qMzoo1SvIr4gykA==","signatures":[{"sig":"MEYCIQDWknZGN5xsOYEoev/vPTfCLwUKwV8Pdva6OfHb0NCuwwIhANgc0FaxWTaMEJFMSyvbar4RvBNNrgxTi+3VTPejh/Xb","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":54828},"main":"./dist/cjs/index.js","type":"module","types":"./dist/types/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"e8eab2385de3264674e5db3e2449201dd3600dd4","scripts":{"try":"tsx playground/try.ts","clean":"rmdir /s /q dist 2>nul || true","build:all":"npm run build:cjs && npm run build:esm && npm run build:types","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json","typecheck":"tsc -p tsconfig.check.json","build:types":"tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/types","prepublishOnly":"npm run typecheck && npm run build:all"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"_npmVersion":"11.10.1","description":"Lightweight SDK for sending events to your tracker service","directories":{},"sideEffects":false,"_nodeVersion":"22.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.21.0","typescript":"^5.4.5","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/raqib_0.0.1_1776206231105_0.37064954470085354","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@abdoseadaa/raqib","version":"0.2.0","keywords":["tracker","analytics","events","sdk"],"license":"MIT","_id":"@abdoseadaa/raqib@0.2.0","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"dist":{"shasum":"7ebc7560e3bc8b7ee94d8f6b7c0258b16cd420b7","tarball":"https://registry.npmjs.org/@abdoseadaa/raqib/-/raqib-0.2.0.tgz","fileCount":72,"integrity":"sha512-QEOJCU7+EjzQjVgE+KvA/YcqHWfSOU1hL9MBt/8OiV7DjJhB2meIAOenepDHhmPDgKm9lreReh29LNRDJZJ+ow==","signatures":[{"sig":"MEUCIH4HnOUML5G2e2tLWB0joDREWSjOrY5fLKuRsumlSpYjAiEAnv8VA1ym6/yPIMxKfRqDSvGNjQtmWnWLZTA/12LSD5Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":69931},"main":"./dist/cjs/index.js","type":"module","types":"./dist/types/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"c181333d90abb42db0168181e8571821e16ba6cd","scripts":{"try":"tsx playground/try.ts","clean":"rmdir /s /q dist 2>nul || true","build:all":"npm run build:cjs && npm run build:esm && npm run build:types","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json","typecheck":"tsc -p tsconfig.check.json","build:types":"tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/types","prepublishOnly":"npm run typecheck && npm run build:all"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"_npmVersion":"11.10.1","description":"Lightweight SDK for sending events to your tracker service","directories":{},"sideEffects":false,"_nodeVersion":"22.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.21.0","typescript":"^5.4.5","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/raqib_0.2.0_1776253544908_0.8553088419155519","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@abdoseadaa/raqib","version":"0.3.0","description":"Lightweight SDK for sending events to your tracker service","type":"module","main":"./dist/cjs/index.js","module":"./dist/esm/index.js","types":"./dist/types/index.d.ts","exports":{".":{"import":"./dist/esm/index.js","require":"./dist/cjs/index.js","types":"./dist/types/index.d.ts"}},"sideEffects":false,"engines":{"node":">=18.0.0"},"scripts":{"build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json","build:types":"tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/types","build:all":"npm run build:cjs && npm run build:esm && npm run build:types","typecheck":"tsc -p tsconfig.check.json","prepublishOnly":"npm run typecheck && npm run build:all","clean":"rmdir /s /q dist 2>nul || true","try":"tsx playground/try.ts"},"keywords":["tracker","analytics","events","sdk"],"license":"MIT","publishConfig":{"access":"public"},"devDependencies":{"@types/node":"^25.6.0","tsx":"^4.21.0","typescript":"^5.4.5"},"gitHead":"02c2ccf9d6d29d50196519ec17f364521aa15080","_id":"@abdoseadaa/raqib@0.3.0","_nodeVersion":"22.20.0","_npmVersion":"11.10.1","dist":{"integrity":"sha512-tkmPJgFQreogmwuZZypz5l2f/+iD0TJ++K+sC46PKlVS4+w/cRci5L7+YeNVcSHBoN2zDjkku3o3XRJ5yZI9Yw==","shasum":"6645768c26cc2e65c7055832c2e8255a768731ef","tarball":"https://registry.npmjs.org/@abdoseadaa/raqib/-/raqib-0.3.0.tgz","fileCount":152,"unpackedSize":170351,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFAQ/g4EZtkj9iDqMDFn4tw4gJySsUIuVbmgIL2xmAFvAiEA2eFVu0D0O0RR8+DtbN2bSwqLGB9wfHl7Xi2nf0xmxCg="}]},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"directories":{},"maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/raqib_0.3.0_1776335826781_0.19176444745944554"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-14T22:37:11.025Z","modified":"2026-04-16T10:37:07.025Z","0.1.0":"2026-04-14T22:33:54.841Z","0.1.1":"2026-04-14T22:35:47.383Z","0.0.1":"2026-04-14T22:37:11.276Z","0.2.0":"2026-04-15T11:45:45.058Z","0.3.0":"2026-04-16T10:37:06.924Z"},"license":"MIT","keywords":["tracker","analytics","events","sdk"],"description":"Lightweight SDK for sending events to your tracker service","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"readme":"# @abdoseadaa/raqib\r\n\r\nLightweight event-tracking SDK. Works in Node.js microservices and the browser. Two lines of setup, one line per event.\r\n\r\n```\r\nnpm install @abdoseadaa/raqib\r\n```\r\n\r\n---\r\n\r\n## Quick start\r\n\r\n```ts\r\nimport { init } from \"@abdoseadaa/raqib\"\r\n\r\nconst tracker = init({\r\n  serviceUrl: process.env.TRACKER_URL,\r\n  apiKey:     process.env.TRACKER_KEY,\r\n})\r\n\r\ntracker.on(\"payment.initiated\", { userId: \"u_123\", amount: 99 })\r\n```\r\n\r\nThat's it. The event is buffered and sent automatically in the background — your code never waits on the tracker.\r\n\r\n---\r\n\r\n## Table of contents\r\n\r\n- [Installation](#installation)\r\n- [init()](#init)\r\n- [tracker.on()](#trackeron)\r\n- [tracker.setContext()](#trackersetcontext)\r\n- [tracker.hook()](#trackerhook)\r\n- [tracker.trackError()](#trackertrackererror)\r\n- [env callback](#env-callback)\r\n- [Encryption](#encryption)\r\n- [Graceful shutdown](#graceful-shutdown)\r\n- [Backend middleware](#backend-middleware)\r\n- [How batching works](#how-batching-works)\r\n- [Wire format](#wire-format)\r\n- [Config reference](#config-reference)\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @abdoseadaa/raqib\r\n```\r\n\r\n**Requirements:** Node.js 18+ (uses native `fetch` and `crypto.subtle`)\r\n\r\n---\r\n\r\n## `init()`\r\n\r\nCreates and returns a `Tracker` instance bound to your config. Throws synchronously if any required field is missing — misconfiguration is caught at startup, not silently at runtime.\r\n\r\n```ts\r\nimport { init } from \"@abdoseadaa/raqib\"\r\n\r\nconst tracker = init({\r\n  serviceUrl: process.env.TRACKER_URL,   // required\r\n  apiKey:     process.env.TRACKER_KEY,   // required\r\n})\r\n```\r\n\r\nYou can create multiple independent instances in the same process — each carries its own config, queue, context, and hooks. This is useful when sending to different tracker service deployments with different base URLs:\r\n\r\n```ts\r\nconst stagingTracker    = init({ serviceUrl: \"https://tracker.staging.internal\", apiKey: \"et_live_stg_...\" })\r\nconst productionTracker = init({ serviceUrl: \"https://tracker.prod.internal\",    apiKey: \"et_live_prd_...\" })\r\n```\r\n\r\n> The `apiKey` controls authentication only — it does not route events to a different destination. If you are sending to the same service URL, one instance is all you need.\r\n\r\n### Higher-order event senders\r\n\r\nThe more useful pattern is creating pre-bound senders from a single instance. Instead of repeating the event name everywhere, wrap it in a function that only takes the payload:\r\n\r\n```ts\r\nconst tracker = init({\r\n  serviceUrl: process.env.TRACKER_URL,\r\n  apiKey:     process.env.TRACKER_KEY,\r\n})\r\n\r\n// Pre-bind event names — callers only pass the payload\r\nconst onPaymentInitiated  = (payload: Record<string, unknown>) => tracker.on(\"payment.initiated\",  payload)\r\nconst onPaymentSucceeded  = (payload: Record<string, unknown>) => tracker.on(\"payment.succeeded\",  payload)\r\nconst onPaymentFailed     = (payload: Record<string, unknown>) => tracker.on(\"payment.failed\",     payload)\r\nconst onUserSignedIn      = (payload: Record<string, unknown>) => tracker.on(\"user.signed_in\",     payload)\r\n\r\n// Usage — clean, no event name string to remember or mistype\r\nonPaymentInitiated({ userId: \"u_123\", amount: 99 })\r\nonPaymentSucceeded({ userId: \"u_123\", transactionId: \"txn_456\" })\r\nonUserSignedIn({ userId: \"u_123\", method: \"email\" })\r\n```\r\n\r\nOr use a factory to generate them:\r\n\r\n```ts\r\nfunction bindEvent(eventName: string) {\r\n  return (payload?: Record<string, unknown>) => tracker.on(eventName, payload ?? {})\r\n}\r\n\r\nconst events = {\r\n  payment: {\r\n    initiated: bindEvent(\"payment.initiated\"),\r\n    succeeded: bindEvent(\"payment.succeeded\"),\r\n    failed:    bindEvent(\"payment.failed\"),\r\n  },\r\n  user: {\r\n    signedIn:  bindEvent(\"user.signed_in\"),\r\n    signedOut: bindEvent(\"user.signed_out\"),\r\n  },\r\n}\r\n\r\n// Usage — fully typed, autocomplete-friendly\r\nevents.payment.initiated({ userId: \"u_123\", amount: 99 })\r\nevents.user.signedIn({ userId: \"u_123\", method: \"oauth\" })\r\n```\r\n\r\n---\r\n\r\n## `tracker.on()`\r\n\r\nFire-and-forget event tracking. Always returns synchronously. Never throws.\r\n\r\n```ts\r\ntracker.on(\"event.name\")\r\ntracker.on(\"event.name\", { key: \"value\" })\r\n```\r\n\r\n**Examples:**\r\n\r\n```ts\r\n// Simple event\r\ntracker.on(\"page.viewed\", { page: \"/dashboard\" })\r\n\r\n// With nested properties\r\ntracker.on(\"order.placed\", {\r\n  orderId: \"ord_001\",\r\n  total:   149,\r\n  items:   [{ sku: \"SKU-01\", qty: 2 }],\r\n})\r\n\r\n// No properties needed\r\ntracker.on(\"app.started\")\r\n```\r\n\r\n---\r\n\r\n## `tracker.setContext()`\r\n\r\nSets persistent fields that are automatically merged into **every subsequent event**. Calling it again extends (never replaces) the existing context.\r\n\r\n```ts\r\ntracker.setContext({ userId: \"u_123\", plan: \"pro\" })\r\n\r\ntracker.on(\"page.viewed\",    { page: \"/dashboard\" })\r\n// → properties: { userId: \"u_123\", plan: \"pro\", page: \"/dashboard\" }\r\n\r\ntracker.on(\"button.clicked\", { button: \"upgrade\" })\r\n// → properties: { userId: \"u_123\", plan: \"pro\", button: \"upgrade\" }\r\n\r\n// Extend context later — merges with existing\r\ntracker.setContext({ sessionId: \"sess_abc\" })\r\n// Now every event also carries sessionId\r\n```\r\n\r\n**Priority:** `setContext` fields are overridden by `hook()` and by explicit `on()` properties.\r\n\r\n---\r\n\r\n## `tracker.hook()`\r\n\r\nRegisters a **dynamic callback** that is called fresh on every event. Returns an `unregister` function.\r\n\r\nUnlike `setContext()` which stores static values, a hook runs a function each time — so it can read live request data, session state, cookies, or anything else available in the current scope.\r\n\r\n```ts\r\nconst unhook = tracker.hook(() => ({\r\n  userId:    getCurrentUserId(),\r\n  sessionId: getCurrentSessionId(),\r\n}))\r\n\r\ntracker.on(\"page.viewed\", { page: \"/profile\" })\r\n// → properties: { userId: \"u_123\", sessionId: \"sess_abc\", page: \"/profile\" }\r\n\r\nunhook()   // remove the hook when done\r\n```\r\n\r\n### Use in Express middleware\r\n\r\nThe most powerful use case — inject request-scoped data without touching every `tracker.on()` call:\r\n\r\n```ts\r\nimport express from \"express\"\r\n\r\nconst app = express()\r\n\r\napp.use((req, res, next) => {\r\n  const unhook = tracker.hook(() => ({\r\n    userId:    req.user?.id,\r\n    requestId: req.headers[\"x-request-id\"],\r\n    ip:        req.ip,\r\n    userAgent: req.headers[\"user-agent\"],\r\n  }))\r\n\r\n  res.on(\"finish\", unhook)   // automatically clean up when response ends\r\n  next()\r\n})\r\n\r\n// Now every tracker.on() in any route automatically carries userId and requestId\r\napp.post(\"/api/checkout\", (req, res) => {\r\n  tracker.on(\"checkout.started\", { cartId: req.body.cartId })\r\n  // → properties: { userId: \"u_123\", requestId: \"req_abc\", ip: \"...\", cartId: \"cart_1\" }\r\n})\r\n```\r\n\r\n### Multiple hooks\r\n\r\nEach `hook()` call registers an independent callback. All hooks run on every event and their results merge:\r\n\r\n```ts\r\nconst unhook1 = tracker.hook(() => ({ userId: req.user.id }))\r\nconst unhook2 = tracker.hook(() => ({ region: getRegion() }))\r\nconst unhook3 = tracker.hook(() => ({ featureFlags: getFlags() }))\r\n\r\n// All three inject into every event\r\n// Clean up each independently\r\nres.on(\"finish\", () => { unhook1(); unhook2(); unhook3() })\r\n```\r\n\r\n**Priority:** hook data overrides `setContext` but is overridden by explicit `on()` properties.\r\n\r\n---\r\n\r\n## `tracker.trackError()`\r\n\r\nShorthand for exception tracking. Normalises an `Error` object into `{ name, message, stack }` and fires it under the reserved event name `sdk.error`.\r\n\r\n```ts\r\ntry {\r\n  await chargeCard({ userId: \"u_123\", amount: 99 })\r\n} catch (err) {\r\n  tracker.trackError(err as Error, { route: \"/api/checkout\", userId: \"u_123\" })\r\n}\r\n```\r\n\r\nThe resulting event:\r\n```json\r\n{\r\n  \"event_name\": \"sdk.error\",\r\n  \"properties\": {\r\n    \"route\": \"/api/checkout\",\r\n    \"userId\": \"u_123\",\r\n    \"error\": {\r\n      \"name\":    \"CardDeclinedError\",\r\n      \"message\": \"Card was declined by the issuer\",\r\n      \"stack\":   \"CardDeclinedError: ...\\n    at ...\"\r\n    }\r\n  }\r\n}\r\n```\r\n\r\nAny active `setContext` or `hook` data is also merged in automatically.\r\n\r\n---\r\n\r\n## `env` callback\r\n\r\nA function defined once in `init()` that runs on every event and injects its result into `properties.env`. Use it for metadata you always want attached: timestamps, runtime info, browser data, session identifiers.\r\n\r\n```ts\r\n// Node.js / backend\r\nconst tracker = init({\r\n  serviceUrl: process.env.TRACKER_URL,\r\n  apiKey:     process.env.TRACKER_KEY,\r\n  env: () => ({\r\n    timestamp:   new Date().toISOString(),\r\n    serviceName: \"payment-service\",\r\n    nodeVersion: process.version,\r\n    env:         process.env.NODE_ENV,\r\n  }),\r\n})\r\n```\r\n\r\n```ts\r\n// Browser / frontend\r\nconst tracker = init({\r\n  serviceUrl: process.env.TRACKER_URL,\r\n  apiKey:     process.env.TRACKER_KEY,\r\n  env: () => ({\r\n    timestamp:  new Date().toISOString(),\r\n    userAgent:  navigator.userAgent,\r\n    language:   navigator.language,\r\n    screenSize: `${window.innerWidth}x${window.innerHeight}`,\r\n    userId:     localStorage.getItem(\"userId\"),\r\n    sessionId:  sessionStorage.getItem(\"sessionId\"),\r\n  }),\r\n})\r\n```\r\n\r\nThe `env` object is **deep-merged** from all sources. Priority (lowest → highest):\r\n\r\n```\r\nconfig.env() callback\r\n  ↓ merged with\r\nsetContext({ env: { ... } })\r\n  ↓ merged with\r\nhook() returning { env: { ... } }\r\n  ↓ merged with\r\ntracker.on(\"x\", { env: { ... } })   ← explicit call always wins\r\n```\r\n\r\n---\r\n\r\n## Encryption\r\n\r\nWhen `encryptionKey` is set, the entire batch payload is **AES-256-GCM encrypted** before sending. The network tab shows only an opaque base64 blob — event names, property keys, and values are all hidden.\r\n\r\n```ts\r\nconst tracker = init({\r\n  serviceUrl:    process.env.TRACKER_URL,\r\n  apiKey:        process.env.TRACKER_KEY,\r\n  encryptionKey: process.env.TRACKER_ENCRYPTION_KEY,\r\n})\r\n```\r\n\r\n**What the network sees:**\r\n```json\r\n{ \"encrypted\": true, \"d\": \"A1b2C3d4...base64gibberish...==\" }\r\n```\r\n\r\n**Key rules:**\r\n- Any string length — the key is SHA-256 hashed internally to produce a 32-byte AES-256 key\r\n- Both sides must use the exact same string\r\n- Store it as an environment variable — never hardcode it\r\n- Generate a strong key: `openssl rand -base64 32`\r\n\r\n**Backend decryption** — use the included middleware (see [Backend middleware](#backend-middleware)) or implement manually:\r\n\r\n```ts\r\nasync function decryptPayload(d: string, encryptionKey: string) {\r\n  const keyBytes = await crypto.subtle.digest(\r\n    \"SHA-256\",\r\n    new TextEncoder().encode(encryptionKey)\r\n  )\r\n  const key = await crypto.subtle.importKey(\r\n    \"raw\", keyBytes, { name: \"AES-GCM\" }, false, [\"decrypt\"]\r\n  )\r\n  const combined   = Buffer.from(d, \"base64\")\r\n  const iv         = combined.subarray(0, 12)    // first 12 bytes = IV\r\n  const ciphertext = combined.subarray(12)       // rest = ciphertext\r\n  const plaintext  = await crypto.subtle.decrypt({ name: \"AES-GCM\", iv }, key, ciphertext)\r\n  return JSON.parse(new TextDecoder().decode(plaintext))\r\n  // → { events: [{ event_name: \"...\", properties: { ... } }] }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Graceful shutdown\r\n\r\nThe SDK runs a background flush timer. On process termination, stop the timer and drain the buffer so no events are lost:\r\n\r\n```ts\r\nprocess.on(\"SIGTERM\", async () => {\r\n  tracker.destroy()      // stop background timer\r\n  await tracker.flush()  // send remaining buffered events\r\n  process.exit(0)\r\n})\r\n```\r\n\r\nYou can also call `flush()` at any time to force an immediate send:\r\n\r\n```ts\r\n// After a critical event, flush immediately without waiting for the timer\r\ntracker.on(\"payment.succeeded\", { transactionId: \"txn_456\" })\r\nawait tracker.flush()\r\n```\r\n\r\n---\r\n\r\n## Backend middleware\r\n\r\nImport the Express middleware to transparently decrypt incoming payloads from SDK clients that have `encryptionKey` set.\r\n\r\n```ts\r\nimport express from \"express\"\r\nimport { decryptMiddleware } from \"@abdoseadaa/raqib/middleware\"\r\n\r\nconst app = express()\r\n\r\napp.use(express.json())\r\napp.use(decryptMiddleware(process.env.TRACKER_ENCRYPTION_KEY))\r\n\r\napp.post(\"/v1/events/webhook/bulk\", (req, res) => {\r\n  // req.body is always plain JSON here — decrypted transparently if needed\r\n  const { events } = req.body\r\n  // events → [{ event_name: \"...\", properties: { ... } }, ...]\r\n})\r\n```\r\n\r\nUnencrypted requests pass through untouched. The middleware handles both encrypted and plain payloads on the same route.\r\n\r\n**Error responses:**\r\n\r\n| Scenario | Status | Code |\r\n|---|---|---|\r\n| `encrypted: true` but no `d` field | `400` | `INVALID_ENCRYPTED_PAYLOAD` |\r\n| Wrong key or corrupted data | `400` | `DECRYPT_FAILED` |\r\n| Not encrypted | Pass-through | — |\r\n\r\n---\r\n\r\n## How batching works\r\n\r\n`tracker.on()` never blocks. Events go into an in-memory queue and are sent in batches automatically.\r\n\r\n```\r\ntracker.on(\"x\", {...})\r\n       ↓\r\n  in-memory buffer\r\n       ↓\r\n  flush when EITHER:\r\n    → flushIntervalMs elapsed (default: 2000ms)   time trigger\r\n    → batchSize events buffered (default: 20)      count trigger\r\n       ↓\r\n  POST /v1/events/webhook/bulk\r\n  { events: [...up to 20...] }\r\n       ↓\r\n  retry on failure (up to 3×, exponential backoff + jitter)\r\n```\r\n\r\n**Time trigger** — prevents events from going stale in low-traffic periods.\r\n**Count trigger** — prevents oversized payloads during traffic spikes.\r\n\r\nTune both in `init()`:\r\n\r\n```ts\r\nconst tracker = init({\r\n  serviceUrl:      process.env.TRACKER_URL,\r\n  apiKey:          process.env.TRACKER_KEY,\r\n  flushIntervalMs: 5000,   // flush every 5s (default: 2000)\r\n  batchSize:       50,     // or when 50 events buffer up (default: 20)\r\n})\r\n```\r\n\r\n---\r\n\r\n## Wire format\r\n\r\nEvery flush is a single `POST` request:\r\n\r\n```\r\nPOST /v1/events/webhook/bulk\r\nAuthorization: Bearer <apiKey>\r\nContent-Type: application/json\r\n```\r\n\r\n**Plain (no encryption):**\r\n```json\r\n{\r\n  \"events\": [\r\n    {\r\n      \"event_name\": \"payment.initiated\",\r\n      \"properties\": {\r\n        \"userId\": \"u_123\",\r\n        \"amount\": 99,\r\n        \"env\": {\r\n          \"timestamp\":   \"2026-04-15T10:23:45.123Z\",\r\n          \"serviceName\": \"payment-service\"\r\n        }\r\n      }\r\n    },\r\n    {\r\n      \"event_name\": \"payment.succeeded\",\r\n      \"properties\": {\r\n        \"userId\":        \"u_123\",\r\n        \"transactionId\": \"txn_456\",\r\n        \"env\": {\r\n          \"timestamp\":   \"2026-04-15T10:23:45.890Z\",\r\n          \"serviceName\": \"payment-service\"\r\n        }\r\n      }\r\n    }\r\n  ]\r\n}\r\n```\r\n\r\n**Encrypted (`encryptionKey` set):**\r\n```json\r\n{\r\n  \"encrypted\": true,\r\n  \"d\": \"A1b2C3d4E5f6...base64gibberish...==\"\r\n}\r\n```\r\n\r\n---\r\n\r\n## Config reference\r\n\r\n| Option | Type | Required | Default | Description |\r\n|---|---|---|---|---|\r\n| `serviceUrl` | `string` | Yes | — | Base URL of your tracker service |\r\n| `apiKey` | `string` | Yes | — | Org API key (`et_live_…`) sent as `Authorization: Bearer` |\r\n| `env` | `() => object` | No | — | Called on every event — result injected into `properties.env` |\r\n| `encryptionKey` | `string` | No | — | AES-256-GCM encrypt the batch before sending |\r\n| `flushIntervalMs` | `number` | No | `2000` | Max ms an event waits before being sent |\r\n| `batchSize` | `number` | No | `20` | Flush immediately when buffer reaches this size |\r\n","readmeFilename":"README.md"}