@slnka/web
v1.0.0-beta.18
Published
SLNKA Web SDK — Client-side analytics, deep linking, and attribution (CNDP compliant)
Maintainers
Readme
@slnka/web
Lightweight client-side event tracking SDK for SLNK.
- Zero production dependencies - everything hand-rolled
- < 15 KB gzip - smaller than Mixpanel (36 KB), Amplitude (37 KB), Segment (25 KB)
- GDPR + CNDP compliant by default - no tracking without explicit consent
- Offline resilient - localStorage queue for failed sends
- TypeScript first - full type exports, tree-shakeable ESM
Installation
npm / yarn / pnpm
npm install @slnka/web
# or
yarn add @slnka/web
# or
pnpm add @slnka/webNo authentication or registry config needed — @slnka/web is published on the public npm registry.
CDN (script tag)
<script src="https://cdn.jsdelivr.net/npm/@slnka/web/dist/slnka.umd.js"></script>
<script>
slnka.configure('lsk_live_your_api_key', {
serverUrl: 'https://api.slnk.ma',
workspaceId: 'ws_abc123',
});
</script>Quick Start
import slnka from '@slnka/web';
// 1. Configure
slnka.configure('lsk_live_your_api_key', {
serverUrl: 'https://api.slnk.ma',
workspaceId: 'ws_abc123',
});
// 2. Grant consent (required by default — privacy-first mode)
slnka.optIn();
// 3. Track events
slnka.track('button_click', { buttonId: 'cta-hero' });
slnka.page();
slnka.identify('user_123', { plan: 'pro' });API Reference
slnka.configure(apiKey, config)
Configures the SDK. Must be called before any other method.
slnka.configure('lsk_live_your_api_key', {
serverUrl: 'https://your-instance.slnk.ma', // Required
workspaceId: 'ws_abc123',
flushInterval: 5000,
flushSize: 10,
persistence: 'cookie',
respectDnt: true,
respectGpc: true,
requireConsent: true,
queueBeforeConsent: true,
debug: false,
cookieExpiry: 365,
sessionTimeout: 30,
});The apiKey can optionally embed the workspace ID: 'lsk_live_abc123:ws_def456'.
slnka.track(eventName, properties?)
Tracks a custom event.
slnka.track('purchase_completed', {
product: 'Pro Plan',
amount: 49.99,
currency: 'MAD',
});Each event is automatically enriched with:
eventId(UUIDv4)anonymousIduserId(if identified)sessionIdtimestamp(ISO 8601 UTC)context(page URL, referrer, user agent, screen, locale, timezone)
slnka.page(properties?)
Tracks a page view ($page_view event). Page URL, path, title, and referrer are auto-collected.
slnka.page({ section: 'pricing' });slnka.identify(userId, traits?)
Links the current anonymous ID to a known user. Sends a $identify event.
slnka.identify('user_123', {
email: '[email protected]',
plan: 'professional',
company: 'Acme Corp',
});slnka.reset()
Generates a new anonymous ID, clears the user ID, and starts a fresh session. Use on logout.
slnka.reset();slnka.flush()
Manually flushes all queued events. Returns a Promise.
await slnka.flush();slnka.getAnonymousId()
Returns the current anonymous ID.
slnka.getUserId()
Returns the current user ID (or null if not identified).
slnka.setDebug(enabled)
Enables or disables debug logging to the browser console.
slnka.setDebug(true); // See all SDK activity in consoleConsent Management
The SDK is GDPR + CNDP compliant by default: no cookies are written and no events are sent until the user explicitly grants consent.
slnka.optIn(category?)
Grants consent. Without a category argument, grants all categories.
// Grant all categories
slnka.optIn();
// Grant a specific category
slnka.optIn('analytics');When analytics consent is granted, any events tracked before consent (the "pre-consent queue") are automatically flushed.
slnka.optOut(category?)
Revokes consent. Purges the event queue and deletes all SDK cookies/storage.
slnka.optOut();slnka.getConsentState()
Returns the current consent state for all categories.
const state = slnka.getConsentState();
// { analytics: 'granted', marketing: 'pending', functional: 'granted' }Consent Categories
| Category | Purpose |
|----------|---------|
| analytics | Event tracking, page views, user behavior |
| marketing | Campaign attribution, cross-site tracking |
| functional | Feature flags, A/B testing, personalization |
Consent States
| State | Behavior |
|-------|----------|
| pending | Events queued in memory (not sent, no cookies) |
| granted | Events sent, cookies written |
| denied | Events dropped, cookies/storage purged |
Browser Privacy Signals
The SDK respects:
- Do Not Track (DNT):
navigator.doNotTrack === '1'- consent auto-denied - Global Privacy Control (GPC):
navigator.globalPrivacyControl === true- consent auto-denied
Disable with respectDnt: false and/or respectGpc: false.
Privacy Compliance (GDPR + CNDP)
SLNK is designed for compliance with EU GDPR (Regulation 2016/679) and Morocco's CNDP Law 09-08 (Commission Nationale de contrôle de la protection des Données à caractère Personnel). The same technical mechanisms satisfy both frameworks:
- No tracking without consent -
requireConsent: trueis the default. - No cookies before consent - Anonymous ID is held in memory until
optIn(). - Pre-consent queue - Events tracked before consent are buffered in memory (not localStorage) and flushed only after
optIn(). - Full data purge on opt-out -
optOut()deletes all cookies, localStorage entries, and queued events. - DNT and GPC respected - Browser-level privacy signals are honored.
- No raw IP storage - IP hashing happens server-side (SHA-256 + daily salt).
- First-party cookies only -
SameSite=Lax,Secure, on your domain.
Cookie Banner Integration
// Configure SDK (no cookies yet)
slnka.configure('lsk_live_your_api_key', {
serverUrl: 'https://api.slnk.ma',
workspaceId: 'ws_abc123',
});
// Events are tracked but queued in memory
slnka.page();
slnka.track('page_scroll', { depth: 50 });
// User clicks "Accept" on your cookie banner
document.getElementById('accept-cookies').addEventListener('click', () => {
slnka.optIn(); // Flushes all queued events, writes cookies
});
// User clicks "Reject"
document.getElementById('reject-cookies').addEventListener('click', () => {
slnka.optOut(); // Purges everything
});Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| serverUrl | string | Required | Analytics API base URL (e.g., 'https://api.slnk.ma') |
| workspaceId | string | '' | Workspace ID (can embed in apiKey) |
| flushInterval | number | 5000 | Auto-flush interval in ms |
| flushSize | number | 10 | Max buffer size before auto-flush |
| persistence | 'cookie' \| 'localStorage' \| 'memory' | 'cookie' | Where to persist anonymous ID |
| respectDnt | boolean | true | Respect Do Not Track header |
| respectGpc | boolean | true | Respect Global Privacy Control |
| requireConsent | boolean | true | Require explicit consent |
| queueBeforeConsent | boolean | true | Queue events before consent |
| debug | boolean | false | Console debug logging |
| cookieDomain | string | auto-detected | Cookie domain |
| cookieExpiry | number | 365 | Cookie expiry in days |
| sessionTimeout | number | 30 | Session timeout in minutes |
Event Payload
Every event sent to the backend has this shape:
{
"eventName": "button_click",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"anonymousId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"userId": "user_123",
"properties": {
"buttonId": "cta-hero"
},
"context": {
"page": {
"url": "https://example.com/pricing",
"path": "/pricing",
"title": "Pricing - Example",
"referrer": "https://google.com"
},
"browser": {
"userAgent": "Mozilla/5.0 ...",
"language": "fr-MA"
},
"screen": {
"width": 1920,
"height": 1080
},
"locale": "fr-MA",
"timezone": "Africa/Casablanca"
},
"timestamp": "2026-03-15T14:30:00.000Z",
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}Events are batched and sent to POST /api/v1/sdk/events/batch.
Advanced Usage
Multiple Instances
import { SlnkaClient } from '@slnka/web';
const tracker1 = new SlnkaClient();
tracker1.configure('lsk_live_project_a', {
serverUrl: 'https://api.slnk.ma',
workspaceId: 'ws_a',
});
const tracker2 = new SlnkaClient();
tracker2.configure('lsk_live_project_b', {
serverUrl: 'https://api.slnk.ma',
workspaceId: 'ws_b',
});Server-Side Rendering (SSR)
The SDK gracefully handles SSR environments where window, document, and navigator are unavailable. Context fields will be empty strings and storage falls back to in-memory.
Offline Support
When network requests fail, events are automatically persisted to localStorage (key: __slnka_queue). On the next page load, persisted events are restored and retried.
Storage Keys
| Key | Storage | Purpose |
|-----|---------|---------|
| __slnka_anon | cookie / localStorage / memory | Anonymous user ID |
| __slnka_user | cookie / localStorage / memory | Identified user ID |
| __slnka_consent | localStorage | Consent state |
| __slnka_session | sessionStorage | Session ID |
| __slnka_session_activity | sessionStorage | Last activity timestamp |
| __slnka_queue | localStorage | Persisted event queue (offline) |
Browser Support
- Chrome 80+
- Firefox 80+
- Safari 14+
- Edge 80+
- Mobile Safari (iOS 14+)
- Chrome Android 80+
The SDK uses fetch, crypto.getRandomValues, sendBeacon, and Intl.DateTimeFormat, all widely supported in modern browsers.
Clés API et scopes (PH03)
Pour le code mobile et web embarqué, créez une clé de collecte (COLLECT) dans les paramètres du projet. Elle est publiable et accepte uniquement events:write, deeplinks:resolve et feedback:write. Activez les scopes nécessaires à votre intégration. Elle ne donne accès ni aux liens, ni aux rapports, ni à l'administration. Le consentement SDK utilise events:write.
Une clé serveur (SERVER) reste dans un environnement serveur privé. Ses scopes sont explicites : links:read, links:write, links:delete, analytics:read, export, et les trois scopes de collecte. La suppression d'un lien exige links:write et links:delete. Aucun scope ne permet d'administrer les membres, rôles, réglages ou clés API. Les routes /api/v1/sdk/** exigent une clé API ; les utilisateurs du tableau de bord utilisent les routes et permissions RBAC correspondantes.
Les clés existantes restent SERVER. La migration ajoute aux clés non révoquées les trois scopes de collecte afin de préserver les intégrations SDK et remplace tags:read/write par links:read/write. Régénérer une clé conserve sa nature et ses scopes. Une nouvelle clé serveur sans scopes explicites reçoit seulement links:read et links:write.
Friction signals and automatic page views
After analytics consent, enhanced measurement detects unresponsive clicks and quick history returns by default. Configure them independently:
slnka.configure('lsk_live_your_api_key', {
projectId: 'your-project-id',
enhancedMeasurement: {
deadClicks: { enabled: true, thresholdMs: 1000, excludeSelectors: ['.custom-player'] },
quickBacks: { enabled: true, thresholdMs: 3000 },
pageViews: false,
},
});$dead_click: a primary click without modifiers followed by no DOM mutation, URL change, scroll, text selection, focus elsewhere or page departure within the threshold. Inputs, editable elements, labels, media and embedded content are excluded. Adddata-slnk-ignoreto any element to exclude its subtree. At most one signal per selector per five seconds and twenty per page is sent. The payload contains selector, tag, click coordinates and viewport size; element text is never captured. Pure CSS reactions are not observable and can produce false positives: exclude these controls explicitly. An unrelated DOM update can suppress a signal; this is a heuristic, not proof of failure.$quick_back: a history return to the preceding URL in strictly less than three seconds by default. Supports SPA history traversal and same-origin MPA back/forward navigation, including bfcache restoration. URL identity keeps origin, pathname and query, excluding fragments.__slnka_navin sessionStorage carries the consented last visit across documents; blocked storage degrades cross-document detection. Consent revocation/reset clears it.pageViews: truecallspage()after tracker initialization and when the pathname changes. It defaults to false. Remove overlapping manualpage()calls if enabling it. Query-only and fragment-only changes do not add views.
Trackers start only when analytics consent permits collection. Revocation or a
return to pending consent cancels timers/subscriptions; granting consent again
starts a fresh observation period. No pre-consent interaction is reconstructed.
Available from 1.0.0-beta.18.
Server surveys and prompt identity
Server-driven surveys use the SurveyDto contract: string id, an ordered
steps list and uppercase promptType values. The SDK converts these to its
lowercase UI types. An empty or unsupported step list does not start a survey.
Single prompt responses retain the server's promptConfigId and trigger; direct
feedback.show() callers may also supply promptConfigId.
Survey starts send surveyId, anonymousId, sessionId and pageUrl, then read
responseId. Every submitted answer carries its promptConfigId and
surveyResponseId. The UI waits for each response before advancing and for the
last answer before requesting completion. Failed requests keep the current step
available for retry. A failed completion can be retried without resending the
already acknowledged final answer. Closing a survey does not change the callback
used by later standalone prompts.
Available from 1.0.0-beta.18, with a backend exposing GET /api/v1/sdk/feedback/surveys/{id}
and accepting promptConfigId / surveyResponseId on POST /api/v1/sdk/feedback/responses.
