@clianta/sdk 1.5.1 → 1.6.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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to the Clianta SDK will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.6.0] - 2026-03-01
9
+
10
+ ### Added
11
+ - **`group()` method** — Associate visitors with a company/account. The `groupId` is attached to all subsequent `track()` calls, enabling ABM (Account-Based Marketing) use cases
12
+ - **`alias()` method** — Merge two visitor identities (e.g., anonymous visitor → logged-in user). Supports cross-device identity resolution
13
+ - **`screen()` method** — Track screen views for mobile-first PWAs and SPAs. Semantic equivalent of `page()` for app screens
14
+ - **Event middleware API** — `use((event, next) => { ... })` to intercept, transform, or drop events before they're sent. Supports chaining multiple middleware functions
15
+ - **`onReady()` callback** — Register callbacks that fire when the SDK is fully initialized. If already ready, fires immediately
16
+ - **`isReady()` method** — Check initialization state synchronously
17
+ - **React `ErrorBoundary`** — `CliantaProvider` now wraps children in an ErrorBoundary to prevent SDK errors from crashing the host application
18
+ - **React `useCliantaReady()` hook** — Returns `{ isReady, tracker }` for components that need to wait for initialization
19
+ - **React `onError` prop** — `CliantaProvider` accepts an `onError` callback for custom error handling
20
+ - **New types** — `GroupTraits`, `MiddlewareFn` exported from main SDK entry
21
+
22
+ ### Changed
23
+ - `TrackerCore` interface expanded with `group()`, `alias()`, `screen()`, `use()`, `onReady()`, `isReady()` methods
24
+ - React `CliantaContext` now provides `{ tracker, isReady }` instead of just `tracker`
25
+ - `track()` now runs events through the middleware pipeline before queueing
26
+ - Events include `groupId` field when visitor is associated with a group
27
+
8
28
  ## [1.5.1] - 2026-02-28
9
29
 
10
30
  ### Added
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.5.1
2
+ * Clianta SDK v1.6.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -10,7 +10,7 @@
10
10
  * @see SDK_VERSION in core/config.ts
11
11
  */
12
12
  /** SDK Version */
13
- const SDK_VERSION = '1.4.0';
13
+ const SDK_VERSION = '1.6.0';
14
14
  /** Default API endpoint — reads from env or falls back to localhost */
15
15
  const getDefaultApiEndpoint = () => {
16
16
  // Build-time env var (works with Next.js, Vite, CRA, etc.)
@@ -3692,6 +3692,85 @@ class CRMClient {
3692
3692
  }
3693
3693
  }
3694
3694
 
3695
+ /**
3696
+ * Privacy-safe visitor API client.
3697
+ * All methods return data for the current visitor only (no cross-visitor access).
3698
+ */
3699
+ class VisitorClient {
3700
+ constructor(transport, workspaceId, visitorId) {
3701
+ this.transport = transport;
3702
+ this.workspaceId = workspaceId;
3703
+ this.visitorId = visitorId;
3704
+ }
3705
+ /** Update visitorId (e.g. after reset) */
3706
+ setVisitorId(id) {
3707
+ this.visitorId = id;
3708
+ }
3709
+ basePath() {
3710
+ return `/api/public/track/visitor/${this.workspaceId}/${this.visitorId}`;
3711
+ }
3712
+ /**
3713
+ * Get the current visitor's profile from the CRM.
3714
+ * Returns visitor data and linked contact info if identified.
3715
+ */
3716
+ async getProfile() {
3717
+ const result = await this.transport.fetchData(`${this.basePath()}/profile`);
3718
+ if (result.success && result.data) {
3719
+ logger.debug('Visitor profile fetched:', result.data);
3720
+ return result.data;
3721
+ }
3722
+ logger.warn('Failed to fetch visitor profile:', result.error);
3723
+ return null;
3724
+ }
3725
+ /**
3726
+ * Get the current visitor's recent activity/events.
3727
+ * Returns paginated list of tracking events.
3728
+ */
3729
+ async getActivity(options) {
3730
+ const params = {};
3731
+ if (options?.page)
3732
+ params.page = options.page.toString();
3733
+ if (options?.limit)
3734
+ params.limit = options.limit.toString();
3735
+ if (options?.eventType)
3736
+ params.eventType = options.eventType;
3737
+ if (options?.startDate)
3738
+ params.startDate = options.startDate;
3739
+ if (options?.endDate)
3740
+ params.endDate = options.endDate;
3741
+ const result = await this.transport.fetchData(`${this.basePath()}/activity`, params);
3742
+ if (result.success && result.data) {
3743
+ return result.data;
3744
+ }
3745
+ logger.warn('Failed to fetch visitor activity:', result.error);
3746
+ return null;
3747
+ }
3748
+ /**
3749
+ * Get a summarized journey timeline for the current visitor.
3750
+ * Includes top pages, sessions, time spent, and recent activities.
3751
+ */
3752
+ async getTimeline() {
3753
+ const result = await this.transport.fetchData(`${this.basePath()}/timeline`);
3754
+ if (result.success && result.data) {
3755
+ return result.data;
3756
+ }
3757
+ logger.warn('Failed to fetch visitor timeline:', result.error);
3758
+ return null;
3759
+ }
3760
+ /**
3761
+ * Get engagement metrics for the current visitor.
3762
+ * Includes time on site, page views, bounce rate, and engagement score.
3763
+ */
3764
+ async getEngagement() {
3765
+ const result = await this.transport.fetchData(`${this.basePath()}/engagement`);
3766
+ if (result.success && result.data) {
3767
+ return result.data;
3768
+ }
3769
+ logger.warn('Failed to fetch visitor engagement:', result.error);
3770
+ return null;
3771
+ }
3772
+ }
3773
+
3695
3774
  /**
3696
3775
  * Clianta SDK - Main Tracker Class
3697
3776
  * @see SDK_VERSION in core/config.ts
@@ -3705,10 +3784,16 @@ class Tracker {
3705
3784
  this.isInitialized = false;
3706
3785
  /** contactId after a successful identify() call */
3707
3786
  this.contactId = null;
3787
+ /** groupId after a successful group() call */
3788
+ this.groupId = null;
3708
3789
  /** Pending identify retry on next flush */
3709
3790
  this.pendingIdentify = null;
3710
3791
  /** Registered event schemas for validation */
3711
3792
  this.eventSchemas = new Map();
3793
+ /** Event middleware pipeline */
3794
+ this.middlewares = [];
3795
+ /** Ready callbacks */
3796
+ this.readyCallbacks = [];
3712
3797
  if (!workspaceId) {
3713
3798
  throw new Error('[Clianta] Workspace ID is required');
3714
3799
  }
@@ -3734,6 +3819,8 @@ class Tracker {
3734
3819
  this.visitorId = this.createVisitorId();
3735
3820
  this.sessionId = this.createSessionId();
3736
3821
  logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3822
+ // Initialize visitor API client
3823
+ this.visitor = new VisitorClient(this.transport, this.workspaceId, this.visitorId);
3737
3824
  // Security warnings
3738
3825
  if (this.config.apiEndpoint.startsWith('http://') &&
3739
3826
  typeof window !== 'undefined' &&
@@ -3748,6 +3835,16 @@ class Tracker {
3748
3835
  this.initPlugins();
3749
3836
  this.isInitialized = true;
3750
3837
  logger.info('SDK initialized successfully');
3838
+ // Fire ready callbacks
3839
+ for (const cb of this.readyCallbacks) {
3840
+ try {
3841
+ cb();
3842
+ }
3843
+ catch (e) {
3844
+ logger.error('onReady callback error:', e);
3845
+ }
3846
+ }
3847
+ this.readyCallbacks = [];
3751
3848
  }
3752
3849
  /**
3753
3850
  * Create visitor ID based on storage mode
@@ -3860,6 +3957,10 @@ class Tracker {
3860
3957
  if (this.contactId) {
3861
3958
  event.contactId = this.contactId;
3862
3959
  }
3960
+ // Attach groupId if known (from a prior group() call)
3961
+ if (this.groupId) {
3962
+ event.groupId = this.groupId;
3963
+ }
3863
3964
  // Validate event against registered schema (debug mode only)
3864
3965
  this.validateEventSchema(eventType, properties);
3865
3966
  // Check consent before tracking
@@ -3873,8 +3974,11 @@ class Tracker {
3873
3974
  logger.debug('Event dropped (no consent):', eventName);
3874
3975
  return;
3875
3976
  }
3876
- this.queue.push(event);
3877
- logger.debug('Event tracked:', eventName, properties);
3977
+ // Run event through middleware pipeline
3978
+ this.runMiddleware(event, () => {
3979
+ this.queue.push(event);
3980
+ logger.debug('Event tracked:', eventName, properties);
3981
+ });
3878
3982
  }
3879
3983
  /**
3880
3984
  * Track a page view
@@ -3936,80 +4040,47 @@ class Tracker {
3936
4040
  }
3937
4041
  /**
3938
4042
  * Get the current visitor's profile from the CRM.
3939
- * Returns visitor data and linked contact info if identified.
3940
- * Only returns data for the current visitor (privacy-safe for frontend).
4043
+ * @deprecated Use `tracker.visitor.getProfile()` instead.
3941
4044
  */
3942
4045
  async getVisitorProfile() {
3943
4046
  if (!this.isInitialized) {
3944
4047
  logger.warn('SDK not initialized');
3945
4048
  return null;
3946
4049
  }
3947
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3948
- if (result.success && result.data) {
3949
- logger.debug('Visitor profile fetched:', result.data);
3950
- return result.data;
3951
- }
3952
- logger.warn('Failed to fetch visitor profile:', result.error);
3953
- return null;
4050
+ return this.visitor.getProfile();
3954
4051
  }
3955
4052
  /**
3956
4053
  * Get the current visitor's recent activity/events.
3957
- * Returns paginated list of tracking events for this visitor.
4054
+ * @deprecated Use `tracker.visitor.getActivity()` instead.
3958
4055
  */
3959
4056
  async getVisitorActivity(options) {
3960
4057
  if (!this.isInitialized) {
3961
4058
  logger.warn('SDK not initialized');
3962
4059
  return null;
3963
4060
  }
3964
- const params = {};
3965
- if (options?.page)
3966
- params.page = options.page.toString();
3967
- if (options?.limit)
3968
- params.limit = options.limit.toString();
3969
- if (options?.eventType)
3970
- params.eventType = options.eventType;
3971
- if (options?.startDate)
3972
- params.startDate = options.startDate;
3973
- if (options?.endDate)
3974
- params.endDate = options.endDate;
3975
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3976
- if (result.success && result.data) {
3977
- return result.data;
3978
- }
3979
- logger.warn('Failed to fetch visitor activity:', result.error);
3980
- return null;
4061
+ return this.visitor.getActivity(options);
3981
4062
  }
3982
4063
  /**
3983
4064
  * Get a summarized journey timeline for the current visitor.
3984
- * Includes top pages, sessions, time spent, and recent activities.
4065
+ * @deprecated Use `tracker.visitor.getTimeline()` instead.
3985
4066
  */
3986
4067
  async getVisitorTimeline() {
3987
4068
  if (!this.isInitialized) {
3988
4069
  logger.warn('SDK not initialized');
3989
4070
  return null;
3990
4071
  }
3991
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3992
- if (result.success && result.data) {
3993
- return result.data;
3994
- }
3995
- logger.warn('Failed to fetch visitor timeline:', result.error);
3996
- return null;
4072
+ return this.visitor.getTimeline();
3997
4073
  }
3998
4074
  /**
3999
4075
  * Get engagement metrics for the current visitor.
4000
- * Includes time on site, page views, bounce rate, and engagement score.
4076
+ * @deprecated Use `tracker.visitor.getEngagement()` instead.
4001
4077
  */
4002
4078
  async getVisitorEngagement() {
4003
4079
  if (!this.isInitialized) {
4004
4080
  logger.warn('SDK not initialized');
4005
4081
  return null;
4006
4082
  }
4007
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
4008
- if (result.success && result.data) {
4009
- return result.data;
4010
- }
4011
- logger.warn('Failed to fetch visitor engagement:', result.error);
4012
- return null;
4083
+ return this.visitor.getEngagement();
4013
4084
  }
4014
4085
  /**
4015
4086
  * Retry pending identify call
@@ -4040,6 +4111,149 @@ class Tracker {
4040
4111
  logger.enabled = enabled;
4041
4112
  logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
4042
4113
  }
4114
+ // ============================================
4115
+ // GROUP, ALIAS, SCREEN
4116
+ // ============================================
4117
+ /**
4118
+ * Associate the current visitor with a group (company/account).
4119
+ * The groupId will be attached to all subsequent track() calls.
4120
+ */
4121
+ group(groupId, traits = {}) {
4122
+ if (!groupId) {
4123
+ logger.warn('groupId is required for group()');
4124
+ return;
4125
+ }
4126
+ this.groupId = groupId;
4127
+ logger.info('Visitor grouped:', groupId);
4128
+ this.track('group', 'Group Identified', {
4129
+ groupId,
4130
+ ...traits,
4131
+ });
4132
+ }
4133
+ /**
4134
+ * Merge two visitor identities.
4135
+ * Links `previousId` (typically the anonymous visitor) to `newId` (the known user).
4136
+ * If `previousId` is omitted, the current visitorId is used.
4137
+ */
4138
+ async alias(newId, previousId) {
4139
+ if (!newId) {
4140
+ logger.warn('newId is required for alias()');
4141
+ return false;
4142
+ }
4143
+ const prevId = previousId || this.visitorId;
4144
+ logger.info('Aliasing visitor:', { from: prevId, to: newId });
4145
+ try {
4146
+ const url = `${this.config.apiEndpoint}/api/public/track/alias`;
4147
+ const response = await fetch(url, {
4148
+ method: 'POST',
4149
+ headers: { 'Content-Type': 'application/json' },
4150
+ body: JSON.stringify({
4151
+ workspaceId: this.workspaceId,
4152
+ previousId: prevId,
4153
+ newId,
4154
+ }),
4155
+ });
4156
+ if (response.ok) {
4157
+ logger.info('Alias successful');
4158
+ return true;
4159
+ }
4160
+ logger.error('Alias failed:', response.status);
4161
+ return false;
4162
+ }
4163
+ catch (error) {
4164
+ logger.error('Alias request failed:', error);
4165
+ return false;
4166
+ }
4167
+ }
4168
+ /**
4169
+ * Track a screen view (for mobile-first PWAs and SPAs).
4170
+ * Similar to page() but semantically for app screens.
4171
+ */
4172
+ screen(name, properties = {}) {
4173
+ this.track('screen_view', name, {
4174
+ ...properties,
4175
+ screenName: name,
4176
+ });
4177
+ }
4178
+ // ============================================
4179
+ // MIDDLEWARE
4180
+ // ============================================
4181
+ /**
4182
+ * Register event middleware.
4183
+ * Middleware functions receive the event and a `next` callback.
4184
+ * Call `next()` to pass the event through, or don't call it to drop the event.
4185
+ *
4186
+ * @example
4187
+ * tracker.use((event, next) => {
4188
+ * // Strip PII from events
4189
+ * delete event.properties.email;
4190
+ * next(); // pass it through
4191
+ * });
4192
+ */
4193
+ use(middleware) {
4194
+ this.middlewares.push(middleware);
4195
+ logger.debug('Middleware registered');
4196
+ }
4197
+ /**
4198
+ * Run event through the middleware pipeline.
4199
+ * Executes each middleware in order; if any skips `next()`, the event is dropped.
4200
+ */
4201
+ runMiddleware(event, finalCallback) {
4202
+ if (this.middlewares.length === 0) {
4203
+ finalCallback();
4204
+ return;
4205
+ }
4206
+ let index = 0;
4207
+ const middlewares = this.middlewares;
4208
+ const next = () => {
4209
+ index++;
4210
+ if (index < middlewares.length) {
4211
+ try {
4212
+ middlewares[index](event, next);
4213
+ }
4214
+ catch (e) {
4215
+ logger.error('Middleware error:', e);
4216
+ finalCallback();
4217
+ }
4218
+ }
4219
+ else {
4220
+ finalCallback();
4221
+ }
4222
+ };
4223
+ try {
4224
+ middlewares[0](event, next);
4225
+ }
4226
+ catch (e) {
4227
+ logger.error('Middleware error:', e);
4228
+ finalCallback();
4229
+ }
4230
+ }
4231
+ // ============================================
4232
+ // LIFECYCLE
4233
+ // ============================================
4234
+ /**
4235
+ * Register a callback to be invoked when the SDK is fully initialized.
4236
+ * If already initialized, the callback fires immediately.
4237
+ */
4238
+ onReady(callback) {
4239
+ if (this.isInitialized) {
4240
+ try {
4241
+ callback();
4242
+ }
4243
+ catch (e) {
4244
+ logger.error('onReady callback error:', e);
4245
+ }
4246
+ }
4247
+ else {
4248
+ this.readyCallbacks.push(callback);
4249
+ }
4250
+ }
4251
+ /**
4252
+ * Check if the SDK is fully initialized and ready.
4253
+ */
4254
+ isReady() {
4255
+ return this.isInitialized;
4256
+ }
4043
4257
  /**
4044
4258
  * Register a schema for event validation.
4045
4259
  * When debug mode is enabled, events will be validated against registered schemas.