@guidelytech/sdk-core 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guidely
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -51,6 +51,7 @@ The module is safe to import during SSR (nothing touches `window` at module scop
51
51
  | `user` | — | `{ id, traits?, account? }` to identify immediately. |
52
52
  | `fetch` | global | Custom `fetch` (tests, polyfills). |
53
53
  | `disableAuthoring` | `false` | Ignore `?guidely_authoring=` in the URL. |
54
+ | `flags` | on | Feature flags (same key and API URL). `false` turns them off; an object passes [flags client options](../flags/README.md#options), e.g. `{ payload }` (server-rendered bootstrap), `{ attributes }`, `{ remoteEval: true }`, `{ pollingMode: "stream" }`. |
54
55
 
55
56
  ## API
56
57
 
@@ -86,6 +87,10 @@ event, never thrown into the host application.
86
87
  | `getState()` | Debug snapshot: ready flag, ids, config version, active/queued guides, widgets, state, pending events. |
87
88
  | `destroy()` | Full cleanup: guides, widgets, listeners, timers, the `#guidely-root` host; flushes pending events via `sendBeacon`. A new client can be created afterwards. |
88
89
  | `GuidelyClient.init(options)` / `GuidelyClient.get()` | Singleton helpers used by the embed (`window.Guidely.init/get`). |
90
+ | `isFeatureEnabled(key, fallback = false)` | Boolean flag for the current user (`fallback` until loaded, when missing or not a boolean). |
91
+ | `getFeatureValue(key, fallback)` / `getFeatureDetails(key, fallback)` | Flag value / value plus reason, rule, variation and experiment. |
92
+ | `onFlagsChange(cb): () => void` | Called whenever flag values may have changed (new rules, identify, navigation, overrides). |
93
+ | `flags` | The [flags client](../flags/README.md): `isEnabled`, `getValue`, `getAll`, `setAttributes`, `setOverrides`, `ready`, `subscribe`… |
89
94
 
90
95
  ### Events
91
96
 
@@ -93,7 +98,26 @@ event, never thrown into the host application.
93
98
  `guide:complete(guide)`, `guide:dismiss(guide)`, `guide:skip(guide)`,
94
99
  `checklist:task({ guide, task, progress })`, `checklist:complete(guide)`,
95
100
  `survey:submit({ guide, answers, responseId })`, `resource-center:open()`, `error(Error)`,
96
- `track(sdkEvent)`, `identify({ userId, anonymousId })`, `authoring({ active })`.
101
+ `track(sdkEvent)`, `identify({ userId, anonymousId })`, `authoring({ active })`, `flags:ready()`,
102
+ `flags:change()`.
103
+
104
+ ## Feature flags
105
+
106
+ ```ts
107
+ if (guidely.isFeatureEnabled("new-checkout")) showNewCheckout();
108
+ const color = guidely.getFeatureValue("checkout-button-color", "blue");
109
+ guidely.onFlagsChange(() => rerender());
110
+ guidely.flags.updateAttributes({ cohort: "beta" }); // extra attributes, kept across identify()
111
+ ```
112
+
113
+ Flags load in parallel with the guide config, with the same public key, and the last payload is
114
+ cached in storage (`guidely:flags:<key>`). Attributes are maintained for you: `id` (the identified
115
+ user id, else the anonymous id), `anonymousId`, every identify trait, `account` (`{ id, …traits }`),
116
+ `$audiences` (audience ids from identify), `url`, `path`, `deviceType`, `browser`, `os`, `locale`,
117
+ `environment` and `platform: "web"`; they update on `identify`, `updateTraits`, `setAccount`,
118
+ `reset` and navigation. Experiment exposures are tracked as `experiment_viewed` events
119
+ (`experimentKey`, `variationId`, `variationName`, `flagKey`, `hashAttribute`) once per session,
120
+ through the event queue, so consent and `analytics: false` apply.
97
121
 
98
122
  ## Behaviour
99
123
 
@@ -147,7 +171,7 @@ Messages posted to the opener: `guidely:authoring-ready { sessionId, url }`, `gu
147
171
  ## Storage keys
148
172
 
149
173
  `guidely:anon`, `guidely:seen`, `guidely:config:<key>`, `guidely:etag:<key>`, `guidely:state:<key>`,
150
- `guidely:user:<key>`, `guidely:queue:<key>` (local/session storage per `storage` option);
174
+ `guidely:user:<key>`, `guidely:queue:<key>`, `guidely:flags:<key>` (local/session storage per `storage` option);
151
175
  `guidely:session`, `guidely:active`, `guidely:authoring` (sessionStorage).
152
176
 
153
177
  ## Network recording (session replay)
@@ -181,11 +205,12 @@ lazily loaded recorder chunk; the base bundle does not grow.
181
205
 
182
206
  | Bundle | minified | min+gz |
183
207
  |---|---|---|
184
- | sdk-core + guide-runtime + floating-ui + `@guidelytech/shared/runtime` (everything an app ships) | 142 kB | **44.4 kB** |
208
+ | sdk-core + guide-runtime + floating-ui + `@guidelytech/flags` (everything an app ships, excluding `@guidelytech/shared`) | 195 kB | **61.4 kB** |
185
209
  | Scout messenger chunk (`@guidelytech/guide-runtime/messenger`, loaded on demand) | 26 kB | 8.8 kB |
186
210
  | Session-replay chunk (`./recorder`: rrweb + replay controller + network recording, loaded only when a session is recorded) | 94 kB | 30 kB |
187
211
  | script-tag build `guidely.js` (authoring schemas and messenger inlined) | 244 kB | ~71 kB |
188
212
 
189
213
  The SDK imports only the zod-free `@guidelytech/shared/runtime` entry; the Zod schemas are loaded
190
- lazily and only when the page enters authoring mode. Budget: 45 kB min+gz for the npm runtime and
214
+ lazily and only when the page enters authoring mode. Feature flags (client + evaluator) add
215
+ ~13 kB min+gz. Budget: 45 kB min+gz for the npm runtime and
191
216
  15 kB for the messenger chunk, 80 kB for the session-replay chunk.
@@ -1,6 +1,7 @@
1
1
  export { A as Api, a as ApiResponse, F as FetchLike, K as KEYS, b as KeyValueStore, c as backoffMs, m as memoryStore, p as postWithRetry, r as readJson, s as sleep, w as writeJson } from './http-47tiAlOM.cjs';
2
- export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-d4eOerxi.cjs';
2
+ export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DbKQPvFt.cjs';
3
3
  import '@guidelytech/shared';
4
+ import '@guidelytech/flags';
4
5
 
5
6
  type AnyFn = (...args: any[]) => void;
6
7
  /** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */
@@ -1,6 +1,7 @@
1
1
  export { A as Api, a as ApiResponse, F as FetchLike, K as KEYS, b as KeyValueStore, c as backoffMs, m as memoryStore, p as postWithRetry, r as readJson, s as sleep, w as writeJson } from './http-47tiAlOM.js';
2
- export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-RyeRQd9f.js';
2
+ export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DqhNl5Am.js';
3
3
  import '@guidelytech/shared';
4
+ import '@guidelytech/flags';
4
5
 
5
6
  type AnyFn = (...args: any[]) => void;
6
7
  /** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */
package/dist/index.cjs CHANGED
@@ -60,6 +60,7 @@ module.exports = __toCommonJS(index_exports);
60
60
  var import_runtime13 = require("@guidelytech/shared/runtime");
61
61
  var import_guide_runtime4 = require("@guidelytech/guide-runtime");
62
62
  var import_guide_runtime5 = require("@guidelytech/guide-runtime");
63
+ var import_flags = require("@guidelytech/flags");
63
64
 
64
65
  // src/authoring.ts
65
66
  var import_guide_runtime = require("@guidelytech/guide-runtime");
@@ -1456,7 +1457,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1456
1457
 
1457
1458
  // src/client.ts
1458
1459
  var SDK_NAME = "@guidelytech/sdk-core";
1459
- var SDK_VERSION = "0.6.2";
1460
+ var SDK_VERSION = "0.7.0";
1460
1461
  var DEFAULT_API_URL = "https://api.guidely.app";
1461
1462
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1462
1463
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1526,6 +1527,10 @@ var GuidelyClient = class _GuidelyClient {
1526
1527
  __publicField(this, "initialPageViewSent", false);
1527
1528
  __publicField(this, "pageViewUrl", null);
1528
1529
  __publicField(this, "pageLeft", false);
1530
+ /** Feature flags for this environment: `guidely.flags.isEnabled("new-checkout")`. */
1531
+ __publicField(this, "flags");
1532
+ /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1533
+ __publicField(this, "exposures", /* @__PURE__ */ new Set());
1529
1534
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1530
1535
  __publicField(this, "eventBus", {
1531
1536
  on: (name, cb) => {
@@ -1584,6 +1589,39 @@ var GuidelyClient = class _GuidelyClient {
1584
1589
  onError: (err) => this.reportError(err)
1585
1590
  });
1586
1591
  if (options.user) this.user = { ...options.user };
1592
+ this.flags = this.createFlags(options.flags);
1593
+ }
1594
+ createFlags(opts) {
1595
+ const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
1596
+ if (opts === false) return (0, import_flags.createFlagsClient)({ ...base, overrideBehaviour: "localOnly", offline: true, reportUsage: false, trackExposures: false, storage: false, logger: null });
1597
+ const o = opts ?? {};
1598
+ return (0, import_flags.createFlagsClient)({
1599
+ ...base,
1600
+ storage: { getItem: (k) => this.store.get(k), setItem: (k, v) => this.store.set(k, v), removeItem: (k) => this.store.remove(k) },
1601
+ reportUsage: this.options.analytics,
1602
+ // Failures surface through onError (and `debug` logs); development warnings (unknown flag keys) reach the console.
1603
+ logger: { warn: (...a) => console.warn("[Guidely]", ...a), error: (...a) => this.log(...a) },
1604
+ ...o,
1605
+ contextAttributes: this.flagContext(),
1606
+ // Exposures travel through the event pipeline (with the session) as `experiment_viewed`.
1607
+ trackExposures: false,
1608
+ onFlagEvaluated: (result) => {
1609
+ this.trackExposure(result);
1610
+ o.onFlagEvaluated?.(result);
1611
+ },
1612
+ onReady: () => {
1613
+ this.emitter.emit("flags:ready");
1614
+ o.onReady?.();
1615
+ },
1616
+ onChange: (data) => {
1617
+ this.emitter.emit("flags:change");
1618
+ o.onChange?.(data);
1619
+ },
1620
+ onError: (err) => {
1621
+ this.reportError(err);
1622
+ o.onError?.(err);
1623
+ }
1624
+ });
1587
1625
  }
1588
1626
  // ---------------------------------------------------------------------------
1589
1627
  // Lifecycle
@@ -1784,6 +1822,7 @@ var GuidelyClient = class _GuidelyClient {
1784
1822
  this.replay = null;
1785
1823
  this.queue.flushSync();
1786
1824
  this.queue.destroy();
1825
+ this.flags.destroy();
1787
1826
  this.emitter.clear();
1788
1827
  (0, import_guide_runtime4.destroyHost)();
1789
1828
  if (current === this) current = null;
@@ -1797,6 +1836,7 @@ var GuidelyClient = class _GuidelyClient {
1797
1836
  this.identitySettled = true;
1798
1837
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
1799
1838
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
1839
+ this.syncFlagAttributes();
1800
1840
  this.enqueue("identify", { properties: { traits: (0, import_runtime13.sanitizeProperties)(this.user.traits) } });
1801
1841
  await this.syncIdentify();
1802
1842
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
@@ -1837,6 +1877,7 @@ var GuidelyClient = class _GuidelyClient {
1837
1877
  };
1838
1878
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
1839
1879
  this.persistState();
1880
+ this.syncFlagAttributes();
1840
1881
  this.widgets.forEach((w, id) => {
1841
1882
  const g = this.findGuide(id);
1842
1883
  if (w.checklist && g) w.checklist.setProgress(this.getChecklistProgress(g.id));
@@ -1848,6 +1889,7 @@ var GuidelyClient = class _GuidelyClient {
1848
1889
  if (!this.user) {
1849
1890
  this.user = { id: "", account };
1850
1891
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
1892
+ this.syncFlagAttributes();
1851
1893
  return;
1852
1894
  }
1853
1895
  await this.identify(this.user.id, void 0, account);
@@ -1856,6 +1898,7 @@ var GuidelyClient = class _GuidelyClient {
1856
1898
  async updateTraits(traits) {
1857
1899
  if (!this.user?.id) {
1858
1900
  this.user = { id: "", traits: { ...this.user?.traits || {}, ...traits } };
1901
+ this.syncFlagAttributes();
1859
1902
  return;
1860
1903
  }
1861
1904
  await this.identify(this.user.id, traits, this.user.account);
@@ -1881,6 +1924,8 @@ var GuidelyClient = class _GuidelyClient {
1881
1924
  this.widgets.clear();
1882
1925
  forgetConversations(this.store, this.options.projectId);
1883
1926
  this.destroyMessenger();
1927
+ this.exposures.clear();
1928
+ this.syncFlagAttributes();
1884
1929
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
1885
1930
  if (this.isReady) {
1886
1931
  this.renderPersistentWidgets();
@@ -1953,6 +1998,7 @@ var GuidelyClient = class _GuidelyClient {
1953
1998
  if (this.destroyed) return;
1954
1999
  const changed = url !== this.lastUrl;
1955
2000
  this.lastUrl = url;
2001
+ if (changed) this.syncFlagAttributes();
1956
2002
  if (!explicit && changed) {
1957
2003
  this.leavePage(url);
1958
2004
  this.trackPageView(url);
@@ -2884,6 +2930,58 @@ var GuidelyClient = class _GuidelyClient {
2884
2930
  if (!this.config?.messenger?.enabled) return;
2885
2931
  void this.loadMessenger().then((m) => m?.showNewMessage(text));
2886
2932
  }
2933
+ // ---------------------------------------------------------------------------
2934
+ // Feature flags
2935
+ // ---------------------------------------------------------------------------
2936
+ /** Boolean flag value for the current user; `fallback` (default `false`) until loaded or when missing. */
2937
+ isFeatureEnabled(key, fallback = false) {
2938
+ return this.flags.isEnabled(key, fallback);
2939
+ }
2940
+ /** Flag value for the current user; `fallback` when missing or of a different type. */
2941
+ getFeatureValue(key, fallback) {
2942
+ return this.flags.getValue(key, fallback);
2943
+ }
2944
+ /** Flag value plus why it was served (reason, rule, variation, experiment). */
2945
+ getFeatureDetails(key, fallback) {
2946
+ return this.flags.getDetails(key, fallback);
2947
+ }
2948
+ /** Calls `cb` whenever flag values may have changed (new rules, identity, page, overrides). Returns an unsubscribe function. */
2949
+ onFlagsChange(cb) {
2950
+ return this.flags.subscribe(cb);
2951
+ }
2952
+ /** Attributes flags are evaluated with: identity, traits, account, audiences, page and device. */
2953
+ flagContext() {
2954
+ const device = detectDevice(navigator.userAgent, window.innerWidth);
2955
+ const account = this.user?.account;
2956
+ return {
2957
+ url: location.href,
2958
+ path: location.pathname,
2959
+ deviceType: device.type,
2960
+ browser: device.browser,
2961
+ os: device.os,
2962
+ locale: navigator.language || void 0,
2963
+ environment: this.options.environment,
2964
+ platform: "web",
2965
+ ...this.user?.traits ?? {},
2966
+ id: this.user?.id || this.anonymousId,
2967
+ anonymousId: this.anonymousId,
2968
+ account: account ? { ...account.traits ?? {}, id: account.id } : void 0,
2969
+ $audiences: [...this.state.audienceIds]
2970
+ };
2971
+ }
2972
+ syncFlagAttributes() {
2973
+ if (!this.destroyed) this.flags?.setContextAttributes(this.flagContext());
2974
+ }
2975
+ /** `experiment_viewed`, once per session and assignment, through the event queue (consent applies). */
2976
+ trackExposure(result) {
2977
+ const exp = result.inExperiment ? result.experiment : void 0;
2978
+ if (!exp || this.destroyed) return;
2979
+ const key = `${this.session.id}|${exp.key}|${exp.hashValue}|${exp.variationId}`;
2980
+ if (this.exposures.has(key)) return;
2981
+ if (this.exposures.size > 500) this.exposures.clear();
2982
+ this.exposures.add(key);
2983
+ this.enqueue("experiment_viewed", { properties: { experimentKey: exp.key, variationId: exp.variationId, flagKey: result.key, hashAttribute: exp.hashAttribute, ...exp.variationName ? { variationName: exp.variationName } : {} } });
2984
+ }
2887
2985
  on(event, cb) {
2888
2986
  return this.emitter.on(event, cb);
2889
2987
  }
@@ -2909,7 +3007,8 @@ var GuidelyClient = class _GuidelyClient {
2909
3007
  session: this.session,
2910
3008
  pendingEvents: this.queue.size,
2911
3009
  authoring: !!this.authoring?.isActive(),
2912
- capture: !!this.capture?.isActive()
3010
+ capture: !!this.capture?.isActive(),
3011
+ flagsReady: this.flags.isReady()
2913
3012
  };
2914
3013
  }
2915
3014
  // ---------------------------------------------------------------------------