@guidelytech/sdk-core
v0.9.0
Published
Framework-independent Guidely browser SDK
Maintainers
Readme
@guidelytech/sdk-core
Framework-independent Guidely browser SDK: loads the published configuration for an environment,
identifies users, tracks events, evaluates targeting/trigger/frequency rules and renders guides
through @guidelytech/guide-runtime. Framework adapters (@guidelytech/react, @guidelytech/nextjs,
@guidelytech/svelte, @guidelytech/sveltekit) and the script-tag build (@guidelytech/embed) wrap this
package.
pnpm add @guidelytech/sdk-coreimport { createGuidely } from "@guidelytech/sdk-core";
const guidely = createGuidely({
projectId: "pk_dev_demo_react", // environment public key (not a secret)
apiUrl: "http://localhost:3000", // defaults to https://api.guidely.app
environment: "development",
});
await guidely.ready();
await guidely.identify("user_123", { role: "admin", plan: "pro" }, { id: "acme", traits: { seats: 12 } });
guidely.track("contact_created", { source: "import" });
guidely.on("guide:complete", (guide) => console.log("completed", guide.key));The module is safe to import during SSR (nothing touches window at module scope).
createGuidely() throws a friendly error when called on the server.
Options
| Option | Default | Description |
|---|---|---|
| projectId | — | Environment public key (pk_dev_…, pk_stag_…, pk_prod_…). Required. |
| environment | inferred from key prefix | development | staging | production; exposed to targeting (environment field). |
| apiUrl | https://api.guidely.app | Base URL of the Guidely API. Demos use http://localhost:3000. |
| debug | false | Log trigger decisions and requests with console.debug("[Guidely]", …). |
| analytics | true | Send analytics events. Guides still render when false. |
| consent | "granted" | "pending" buffers events (persisted) until setConsent("granted"); "denied" disables analytics. |
| storage | "local" | "local" | "session" | "memory" for identity, cached config, state and queue. |
| pollIntervalMs | 1000 | Idle re-check interval (min 250ms): re-arms element_visible triggers whose target appears without navigation and drains the guide queue. |
| configRefreshMs | 300000 | Periodic config refresh (If-None-Match → 304 when unchanged). |
| maskSelectors | [] | Elements whose text is never captured: authoring picks, click text, session replays (merged with the project setting, [data-guidely-mask] and .guidely-mask). |
| autocapture | project setting | false or { pageviews?, pageLeave?, clicks?, rageClicks?, heatmaps?, mouseMovement? }: switch product-analytics autocapture off for this installation (options can only turn things off). |
| release | — | App version or commit SHA (≤100 chars) sent with every exception; error stacks are resolved with the source maps uploaded for that release (@guidelytech/sourcemaps). |
| captureErrors | project setting | false stops reporting uncaught errors / unhandled rejections (captureException still works while the project has error tracking on). |
| sessionReplay | project setting | false never records; { maskAllText?, blockSelectors?, recordNetwork?: false, recordNetworkPayloads?: false, maskNetworkRequest? } makes recording stricter (see "Network recording"). |
| onError | — | Receives every error the SDK swallows (network, rendering). |
| theme | — | Overrides for the project theme (primaryColor, backgroundColor, textColor, borderRadius, fontFamily). |
| user | — | { id, traits?, account? } to identify immediately. |
| fetch | global | Custom fetch (tests, polyfills). |
| disableAuthoring | false | Ignore ?guidely_authoring= in the URL. |
| flags | on | Feature flags (same key and API URL). false turns them off; an object passes flags client options, e.g. { payload } (server-rendered bootstrap), { attributes }, { remoteEval: true }, { pollingMode: "stream" }. |
API
All methods are safe to call at any time; failures are reported through onError / the error
event, never thrown into the host application.
| Method | Description |
|---|---|
| init(): Promise<void> / ready(): Promise<void> | Load config (cache first, then network), wire listeners, evaluate triggers. Idempotent. Resolves even when the API is unreachable (cached config is used; an error event is emitted). |
| identify(userId, traits?, account?) | POST /sdk/identify; merges server state (audiences, guide history, checklist progress, submitted surveys). Local state wins when both exist. Re-evaluates triggers (segment_enter, targeting). |
| setAccount({ id, traits? }) | Sets the account for the current user and re-identifies. |
| updateTraits(traits) | Merges traits and re-identifies. |
| reset() | New anonymous id, cleared user and local state, new session; triggers re-evaluated. |
| track(name, properties?) | Custom event. Drives event triggers, advanceOn: "event" steps and checklist tasks (completion: "event"). Keys matching SENSITIVE_PROPERTY_PATTERN are stripped. |
| page(url?) | Records a page_view and re-evaluates triggers. Called automatically on SPA navigation (History API patch + popstate + hashchange); adapters call it explicitly too. |
| setConsent("granted" \| "denied" \| "pending") | Runtime consent changes (anything but granted stops and discards a session recording). |
| captureException(error, properties?) | Reports a caught error as an exception event (source: "manual", handled: true). A no-op when the project has error tracking off; captureErrors: false does not affect it. |
| startSessionRecording() / stopSessionRecording() | Force recording on for this session (ignores sampling; needs replay enabled on the project) / stop it for the rest of the session. |
| isRecording() / getSessionId() | Whether the session is being recorded; the current analytics session id (also the replay id). |
| startGuide(idOrKey, { stepId? }) | Starts a guide regardless of targeting/frequency, replacing any active guide. Checklists open their panel. Returns false for unknown guides. |
| stopGuide() | Removes the active guide silently (no dismiss event). |
| dismissGuide() | Dismisses the active guide (records guide_dismissed). |
| getActiveGuide(): PublishedGuide \| null | The exclusive guide currently showing. |
| getGuides(): readonly PublishedGuide[] | Published guides for this environment. |
| isGuideCompleted(idOrKey) | From merged local + server state. |
| getChecklistProgress(idOrKey) | { completedTaskIds, completedAt, dismissedAt }. |
| completeChecklistTask(idOrKey, taskId) | Manual completion; syncs to /sdk/checklists/:id/progress. |
| openChecklist(idOrKey) / closeChecklist(idOrKey) | Show/hide the checklist panel (re-renders a dismissed checklist). |
| openResourceCenter() / closeResourceCenter() | Toggle the resource center panel. |
| refresh() | Re-fetch config now. |
| on(event, cb): () => void / off(event, cb) | Subscribe to events (below). |
| getContext() | The TargetingContext used for rule evaluation (debugging). |
| getState() | Debug snapshot: ready flag, ids, config version, active/queued guides, widgets, state, pending events. |
| destroy() | Full cleanup: guides, widgets, listeners, timers, the #guidely-root host; flushes pending events via sendBeacon. A new client can be created afterwards. |
| GuidelyClient.init(options) / GuidelyClient.get() | Singleton helpers used by the embed (window.Guidely.init/get). |
| isFeatureEnabled(key, fallback = false) | Boolean flag for the current user (fallback until loaded, when missing or not a boolean). |
| getFeatureValue(key, fallback) / getFeatureDetails(key, fallback) | Flag value / value plus reason, rule, variation and experiment. |
| onFlagsChange(cb): () => void | Called whenever flag values may have changed (new rules, identify, navigation, overrides). |
| flags | The flags client: isEnabled, getValue, getAll, setAttributes, setOverrides, ready, subscribe… |
Events
ready(config), config(config), guide:start(guide), guide:step({ guide, stepId, stepIndex }),
guide:complete(guide), guide:dismiss(guide), guide:skip(guide),
checklist:task({ guide, task, progress }), checklist:complete(guide),
survey:submit({ guide, answers, responseId }), resource-center:open(), error(Error),
track(sdkEvent), identify({ userId, anonymousId }), authoring({ active }), flags:ready(),
flags:change().
Feature flags
if (guidely.isFeatureEnabled("new-checkout")) showNewCheckout();
const color = guidely.getFeatureValue("checkout-button-color", "blue");
guidely.onFlagsChange(() => rerender());
guidely.flags.updateAttributes({ cohort: "beta" }); // extra attributes, kept across identify()Flags load in parallel with the guide config, with the same public key, and the last payload is
cached in storage (guidely:flags:<key>). Attributes are maintained for you: id (the identified
user id, else the anonymous id), anonymousId, every identify trait, account ({ id, …traits }),
$audiences (audience ids from identify), url, path, deviceType, browser, os, locale,
environment and platform: "web"; they update on identify, updateTraits, setAccount,
reset and navigation. Experiment exposures are tracked as experiment_viewed events
(experimentKey, variationId, variationName, flagKey, hashAttribute) once per session,
through the event queue, so consent and analytics: false apply.
Behaviour
- Config:
GET /api/v1/sdk/config?key=…withIf-None-Match; the last good response is cached in storage (guidely:config:<key>) and used when offline. Refreshed everyconfigRefreshMsand onrefresh(). - Events: queued in memory + storage (max 200, oldest dropped), deduped by
eventId, flushed in batches of ≤50 every 2s, on navigation and onvisibilitychange/pagehide(viasendBeacon, falling back tofetch). Failed batches retry with exponential backoff (1s → 30s, 5 attempts) and stay queued afterwards. - Triggers:
page_load,url_match,event,element_click(delegated capture listener),element_visible(IntersectionObserver),delay,after_guide,segment_enter,manual. Filtered byurlPatterns,trigger.urlPattern, targeting rules (rolloutSalt = guide.id), frequency (once,once_per_session,every_time,interval,maxImpressions,cooldownHours,reshowAfterDismiss) and surveystartAt/endAt. - Exclusivity: one flow/modal/tooltip/survey at a time, lowest
priorityfirst,mutuallyExclusiveWithrespected; banners, checklists, hotspot-only guides and the resource center coexist. Remaining eligible guides queue and show after the active one ends (page-scoped triggers are dropped if the page changed). - Multi-page flows:
step.urlsteps wait for navigation; the active step is persisted insessionStorage(guidely:active) so full page loads resume the guide. - Server sync (fire-and-forget with retries):
POST /sdk/guide-state,POST /sdk/checklists/:id/progress,POST /sdk/surveys/:id/responses,GET /sdk/resource-center/search?q=.
Authoring mode
When the URL contains ?guidely_authoring=<token> the SDK:
- Strips the parameter (
history.replaceState) and stores{ token }insessionStorage["guidely:authoring"]so SPA navigations keep authoring active. - Calls
GET /sdk/authoring/session?token=and, when valid, starts the picker from@guidelytech/guide-runtime(toolbar, hover outline, click-to-pick) and a 15s heartbeat (POST /sdk/authoring/session/heartbeat?token=). - Sends each pick as
window.opener.postMessage({ type: "guidely:pick", sessionId, pick }, dashboardOrigin)andPOST /sdk/authoring/session/picks?token=.
Messages accepted from dashboardOrigin (event.origin checked):
| type | Payload | Effect |
|---|---|---|
| guidely:preview-step | { step, theme?, appearance? } | Renders the draft step live (target fallback → modal). |
| guidely:preview-guide | { guide: { id?, key?, name?, kind?, version?, config } } | Runs the draft guide. |
| guidely:highlight | { selectors } | Outlines matches, replies guidely:highlight-result { counts, resolved }. |
| guidely:stop-preview | — | Removes the preview. |
| guidely:ping | — | Replies guidely:pong { url }. |
| guidely:exit | — | Leaves authoring mode. |
Messages posted to the opener: guidely:authoring-ready { sessionId, url }, guidely:pick,
guidely:highlight-result, guidely:preview-ended { reason }, guidely:pong, guidely:exit.
Storage keys
guidely:anon, guidely:seen, guidely:config:<key>, guidely:etag:<key>, guidely:state:<key>,
guidely:user:<key>, guidely:queue:<key>, guidely:flags:<key> (local/session storage per storage option);
guidely:session, guidely:active, guidely:authoring (sessionStorage).
Network recording (session replay)
When the project enables replay with Record network (replay.recordNetwork), recordings include
the page's fetch/XMLHttpRequest calls, WebSocket connections and frames, and resource timing
(scripts, stylesheets, images, fonts, media, the document) as rrweb plugin events
(guidely/network@1, payload NetworkPluginPayload from @guidelytech/shared). It lives in the
lazily loaded recorder chunk; the base bundle does not grow.
fetch,XMLHttpRequestandWebSocketare wrapped only while recording and restored on stop (left as a passthrough if another library wrapped them afterwards). Callers get the original promise / XHR / socket; bodies are read fromresponse.clone()(≤ 2s, never the app's stream).instanceof WebSocket, constants,onmessageandaddEventListenerbehave as before; sockets created while recording keep working after it stops.- Requests to Guidely's SDK API,
data:/blob:URLs and the recorder script are never recorded. URLs are absolute, with sensitive-looking query parameter values replaced by[redacted]. - Headers and bodies only with Record payloads (
replay.recordNetworkPayloads, off by default): header names lower-cased, credentials/cookies[redacted]; bodies only for text, JSON and form-urlencoded content, ≤ 10KB ([truncated]),[binary]otherwise; sensitive JSON/form keys masked. WebSocket frame data ≤ 2KB ([binary <n> B]for binary frames). Frame sizes and totals are always recorded; at most 200 frames per socket and 500 requests per page load. - Batched every second or 20 items, and flushed when the page is hidden.
sessionReplay: { recordNetwork: false }/{ recordNetworkPayloads: false }turn this off for one installation.maskNetworkRequest(request)gets each request after redaction: return a modified copy, ornullto drop it (and, for a WebSocket, all its frames); a throwing hook drops it.
Bundle size
pnpm --filter @guidelytech/sdk-core size builds a minified ESM bundle and prints gzip sizes:
| Bundle | minified | min+gz |
|---|---|---|
| sdk-core + guide-runtime + floating-ui + @guidelytech/flags (everything an app ships, excluding @guidelytech/shared) | 195 kB | 61.4 kB |
| Scout messenger chunk (@guidelytech/guide-runtime/messenger, loaded on demand) | 26 kB | 8.8 kB |
| Session-replay chunk (./recorder: rrweb + replay controller + network recording, loaded only when a session is recorded) | 94 kB | 30 kB |
| script-tag build guidely.js (authoring schemas and messenger inlined) | 244 kB | ~71 kB |
The SDK imports only the zod-free @guidelytech/shared/runtime entry; the Zod schemas are loaded
lazily and only when the page enters authoring mode. Feature flags (client + evaluator) add
~13 kB min+gz. Budget: 45 kB min+gz for the npm runtime and
15 kB for the messenger chunk, 80 kB for the session-replay chunk.
