@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/dist/react.cjs.js CHANGED
@@ -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
  */
@@ -13,7 +13,7 @@ var react = require('react');
13
13
  * @see SDK_VERSION in core/config.ts
14
14
  */
15
15
  /** SDK Version */
16
- const SDK_VERSION = '1.4.0';
16
+ const SDK_VERSION = '1.6.0';
17
17
  /** Default API endpoint — reads from env or falls back to localhost */
18
18
  const getDefaultApiEndpoint = () => {
19
19
  // Build-time env var (works with Next.js, Vite, CRA, etc.)
@@ -3695,6 +3695,85 @@ class CRMClient {
3695
3695
  }
3696
3696
  }
3697
3697
 
3698
+ /**
3699
+ * Privacy-safe visitor API client.
3700
+ * All methods return data for the current visitor only (no cross-visitor access).
3701
+ */
3702
+ class VisitorClient {
3703
+ constructor(transport, workspaceId, visitorId) {
3704
+ this.transport = transport;
3705
+ this.workspaceId = workspaceId;
3706
+ this.visitorId = visitorId;
3707
+ }
3708
+ /** Update visitorId (e.g. after reset) */
3709
+ setVisitorId(id) {
3710
+ this.visitorId = id;
3711
+ }
3712
+ basePath() {
3713
+ return `/api/public/track/visitor/${this.workspaceId}/${this.visitorId}`;
3714
+ }
3715
+ /**
3716
+ * Get the current visitor's profile from the CRM.
3717
+ * Returns visitor data and linked contact info if identified.
3718
+ */
3719
+ async getProfile() {
3720
+ const result = await this.transport.fetchData(`${this.basePath()}/profile`);
3721
+ if (result.success && result.data) {
3722
+ logger.debug('Visitor profile fetched:', result.data);
3723
+ return result.data;
3724
+ }
3725
+ logger.warn('Failed to fetch visitor profile:', result.error);
3726
+ return null;
3727
+ }
3728
+ /**
3729
+ * Get the current visitor's recent activity/events.
3730
+ * Returns paginated list of tracking events.
3731
+ */
3732
+ async getActivity(options) {
3733
+ const params = {};
3734
+ if (options?.page)
3735
+ params.page = options.page.toString();
3736
+ if (options?.limit)
3737
+ params.limit = options.limit.toString();
3738
+ if (options?.eventType)
3739
+ params.eventType = options.eventType;
3740
+ if (options?.startDate)
3741
+ params.startDate = options.startDate;
3742
+ if (options?.endDate)
3743
+ params.endDate = options.endDate;
3744
+ const result = await this.transport.fetchData(`${this.basePath()}/activity`, params);
3745
+ if (result.success && result.data) {
3746
+ return result.data;
3747
+ }
3748
+ logger.warn('Failed to fetch visitor activity:', result.error);
3749
+ return null;
3750
+ }
3751
+ /**
3752
+ * Get a summarized journey timeline for the current visitor.
3753
+ * Includes top pages, sessions, time spent, and recent activities.
3754
+ */
3755
+ async getTimeline() {
3756
+ const result = await this.transport.fetchData(`${this.basePath()}/timeline`);
3757
+ if (result.success && result.data) {
3758
+ return result.data;
3759
+ }
3760
+ logger.warn('Failed to fetch visitor timeline:', result.error);
3761
+ return null;
3762
+ }
3763
+ /**
3764
+ * Get engagement metrics for the current visitor.
3765
+ * Includes time on site, page views, bounce rate, and engagement score.
3766
+ */
3767
+ async getEngagement() {
3768
+ const result = await this.transport.fetchData(`${this.basePath()}/engagement`);
3769
+ if (result.success && result.data) {
3770
+ return result.data;
3771
+ }
3772
+ logger.warn('Failed to fetch visitor engagement:', result.error);
3773
+ return null;
3774
+ }
3775
+ }
3776
+
3698
3777
  /**
3699
3778
  * Clianta SDK - Main Tracker Class
3700
3779
  * @see SDK_VERSION in core/config.ts
@@ -3708,10 +3787,16 @@ class Tracker {
3708
3787
  this.isInitialized = false;
3709
3788
  /** contactId after a successful identify() call */
3710
3789
  this.contactId = null;
3790
+ /** groupId after a successful group() call */
3791
+ this.groupId = null;
3711
3792
  /** Pending identify retry on next flush */
3712
3793
  this.pendingIdentify = null;
3713
3794
  /** Registered event schemas for validation */
3714
3795
  this.eventSchemas = new Map();
3796
+ /** Event middleware pipeline */
3797
+ this.middlewares = [];
3798
+ /** Ready callbacks */
3799
+ this.readyCallbacks = [];
3715
3800
  if (!workspaceId) {
3716
3801
  throw new Error('[Clianta] Workspace ID is required');
3717
3802
  }
@@ -3737,6 +3822,8 @@ class Tracker {
3737
3822
  this.visitorId = this.createVisitorId();
3738
3823
  this.sessionId = this.createSessionId();
3739
3824
  logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3825
+ // Initialize visitor API client
3826
+ this.visitor = new VisitorClient(this.transport, this.workspaceId, this.visitorId);
3740
3827
  // Security warnings
3741
3828
  if (this.config.apiEndpoint.startsWith('http://') &&
3742
3829
  typeof window !== 'undefined' &&
@@ -3751,6 +3838,16 @@ class Tracker {
3751
3838
  this.initPlugins();
3752
3839
  this.isInitialized = true;
3753
3840
  logger.info('SDK initialized successfully');
3841
+ // Fire ready callbacks
3842
+ for (const cb of this.readyCallbacks) {
3843
+ try {
3844
+ cb();
3845
+ }
3846
+ catch (e) {
3847
+ logger.error('onReady callback error:', e);
3848
+ }
3849
+ }
3850
+ this.readyCallbacks = [];
3754
3851
  }
3755
3852
  /**
3756
3853
  * Create visitor ID based on storage mode
@@ -3863,6 +3960,10 @@ class Tracker {
3863
3960
  if (this.contactId) {
3864
3961
  event.contactId = this.contactId;
3865
3962
  }
3963
+ // Attach groupId if known (from a prior group() call)
3964
+ if (this.groupId) {
3965
+ event.groupId = this.groupId;
3966
+ }
3866
3967
  // Validate event against registered schema (debug mode only)
3867
3968
  this.validateEventSchema(eventType, properties);
3868
3969
  // Check consent before tracking
@@ -3876,8 +3977,11 @@ class Tracker {
3876
3977
  logger.debug('Event dropped (no consent):', eventName);
3877
3978
  return;
3878
3979
  }
3879
- this.queue.push(event);
3880
- logger.debug('Event tracked:', eventName, properties);
3980
+ // Run event through middleware pipeline
3981
+ this.runMiddleware(event, () => {
3982
+ this.queue.push(event);
3983
+ logger.debug('Event tracked:', eventName, properties);
3984
+ });
3881
3985
  }
3882
3986
  /**
3883
3987
  * Track a page view
@@ -3939,80 +4043,47 @@ class Tracker {
3939
4043
  }
3940
4044
  /**
3941
4045
  * Get the current visitor's profile from the CRM.
3942
- * Returns visitor data and linked contact info if identified.
3943
- * Only returns data for the current visitor (privacy-safe for frontend).
4046
+ * @deprecated Use `tracker.visitor.getProfile()` instead.
3944
4047
  */
3945
4048
  async getVisitorProfile() {
3946
4049
  if (!this.isInitialized) {
3947
4050
  logger.warn('SDK not initialized');
3948
4051
  return null;
3949
4052
  }
3950
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3951
- if (result.success && result.data) {
3952
- logger.debug('Visitor profile fetched:', result.data);
3953
- return result.data;
3954
- }
3955
- logger.warn('Failed to fetch visitor profile:', result.error);
3956
- return null;
4053
+ return this.visitor.getProfile();
3957
4054
  }
3958
4055
  /**
3959
4056
  * Get the current visitor's recent activity/events.
3960
- * Returns paginated list of tracking events for this visitor.
4057
+ * @deprecated Use `tracker.visitor.getActivity()` instead.
3961
4058
  */
3962
4059
  async getVisitorActivity(options) {
3963
4060
  if (!this.isInitialized) {
3964
4061
  logger.warn('SDK not initialized');
3965
4062
  return null;
3966
4063
  }
3967
- const params = {};
3968
- if (options?.page)
3969
- params.page = options.page.toString();
3970
- if (options?.limit)
3971
- params.limit = options.limit.toString();
3972
- if (options?.eventType)
3973
- params.eventType = options.eventType;
3974
- if (options?.startDate)
3975
- params.startDate = options.startDate;
3976
- if (options?.endDate)
3977
- params.endDate = options.endDate;
3978
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3979
- if (result.success && result.data) {
3980
- return result.data;
3981
- }
3982
- logger.warn('Failed to fetch visitor activity:', result.error);
3983
- return null;
4064
+ return this.visitor.getActivity(options);
3984
4065
  }
3985
4066
  /**
3986
4067
  * Get a summarized journey timeline for the current visitor.
3987
- * Includes top pages, sessions, time spent, and recent activities.
4068
+ * @deprecated Use `tracker.visitor.getTimeline()` instead.
3988
4069
  */
3989
4070
  async getVisitorTimeline() {
3990
4071
  if (!this.isInitialized) {
3991
4072
  logger.warn('SDK not initialized');
3992
4073
  return null;
3993
4074
  }
3994
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3995
- if (result.success && result.data) {
3996
- return result.data;
3997
- }
3998
- logger.warn('Failed to fetch visitor timeline:', result.error);
3999
- return null;
4075
+ return this.visitor.getTimeline();
4000
4076
  }
4001
4077
  /**
4002
4078
  * Get engagement metrics for the current visitor.
4003
- * Includes time on site, page views, bounce rate, and engagement score.
4079
+ * @deprecated Use `tracker.visitor.getEngagement()` instead.
4004
4080
  */
4005
4081
  async getVisitorEngagement() {
4006
4082
  if (!this.isInitialized) {
4007
4083
  logger.warn('SDK not initialized');
4008
4084
  return null;
4009
4085
  }
4010
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
4011
- if (result.success && result.data) {
4012
- return result.data;
4013
- }
4014
- logger.warn('Failed to fetch visitor engagement:', result.error);
4015
- return null;
4086
+ return this.visitor.getEngagement();
4016
4087
  }
4017
4088
  /**
4018
4089
  * Retry pending identify call
@@ -4043,6 +4114,149 @@ class Tracker {
4043
4114
  logger.enabled = enabled;
4044
4115
  logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
4045
4116
  }
4117
+ // ============================================
4118
+ // GROUP, ALIAS, SCREEN
4119
+ // ============================================
4120
+ /**
4121
+ * Associate the current visitor with a group (company/account).
4122
+ * The groupId will be attached to all subsequent track() calls.
4123
+ */
4124
+ group(groupId, traits = {}) {
4125
+ if (!groupId) {
4126
+ logger.warn('groupId is required for group()');
4127
+ return;
4128
+ }
4129
+ this.groupId = groupId;
4130
+ logger.info('Visitor grouped:', groupId);
4131
+ this.track('group', 'Group Identified', {
4132
+ groupId,
4133
+ ...traits,
4134
+ });
4135
+ }
4136
+ /**
4137
+ * Merge two visitor identities.
4138
+ * Links `previousId` (typically the anonymous visitor) to `newId` (the known user).
4139
+ * If `previousId` is omitted, the current visitorId is used.
4140
+ */
4141
+ async alias(newId, previousId) {
4142
+ if (!newId) {
4143
+ logger.warn('newId is required for alias()');
4144
+ return false;
4145
+ }
4146
+ const prevId = previousId || this.visitorId;
4147
+ logger.info('Aliasing visitor:', { from: prevId, to: newId });
4148
+ try {
4149
+ const url = `${this.config.apiEndpoint}/api/public/track/alias`;
4150
+ const response = await fetch(url, {
4151
+ method: 'POST',
4152
+ headers: { 'Content-Type': 'application/json' },
4153
+ body: JSON.stringify({
4154
+ workspaceId: this.workspaceId,
4155
+ previousId: prevId,
4156
+ newId,
4157
+ }),
4158
+ });
4159
+ if (response.ok) {
4160
+ logger.info('Alias successful');
4161
+ return true;
4162
+ }
4163
+ logger.error('Alias failed:', response.status);
4164
+ return false;
4165
+ }
4166
+ catch (error) {
4167
+ logger.error('Alias request failed:', error);
4168
+ return false;
4169
+ }
4170
+ }
4171
+ /**
4172
+ * Track a screen view (for mobile-first PWAs and SPAs).
4173
+ * Similar to page() but semantically for app screens.
4174
+ */
4175
+ screen(name, properties = {}) {
4176
+ this.track('screen_view', name, {
4177
+ ...properties,
4178
+ screenName: name,
4179
+ });
4180
+ }
4181
+ // ============================================
4182
+ // MIDDLEWARE
4183
+ // ============================================
4184
+ /**
4185
+ * Register event middleware.
4186
+ * Middleware functions receive the event and a `next` callback.
4187
+ * Call `next()` to pass the event through, or don't call it to drop the event.
4188
+ *
4189
+ * @example
4190
+ * tracker.use((event, next) => {
4191
+ * // Strip PII from events
4192
+ * delete event.properties.email;
4193
+ * next(); // pass it through
4194
+ * });
4195
+ */
4196
+ use(middleware) {
4197
+ this.middlewares.push(middleware);
4198
+ logger.debug('Middleware registered');
4199
+ }
4200
+ /**
4201
+ * Run event through the middleware pipeline.
4202
+ * Executes each middleware in order; if any skips `next()`, the event is dropped.
4203
+ */
4204
+ runMiddleware(event, finalCallback) {
4205
+ if (this.middlewares.length === 0) {
4206
+ finalCallback();
4207
+ return;
4208
+ }
4209
+ let index = 0;
4210
+ const middlewares = this.middlewares;
4211
+ const next = () => {
4212
+ index++;
4213
+ if (index < middlewares.length) {
4214
+ try {
4215
+ middlewares[index](event, next);
4216
+ }
4217
+ catch (e) {
4218
+ logger.error('Middleware error:', e);
4219
+ finalCallback();
4220
+ }
4221
+ }
4222
+ else {
4223
+ finalCallback();
4224
+ }
4225
+ };
4226
+ try {
4227
+ middlewares[0](event, next);
4228
+ }
4229
+ catch (e) {
4230
+ logger.error('Middleware error:', e);
4231
+ finalCallback();
4232
+ }
4233
+ }
4234
+ // ============================================
4235
+ // LIFECYCLE
4236
+ // ============================================
4237
+ /**
4238
+ * Register a callback to be invoked when the SDK is fully initialized.
4239
+ * If already initialized, the callback fires immediately.
4240
+ */
4241
+ onReady(callback) {
4242
+ if (this.isInitialized) {
4243
+ try {
4244
+ callback();
4245
+ }
4246
+ catch (e) {
4247
+ logger.error('onReady callback error:', e);
4248
+ }
4249
+ }
4250
+ else {
4251
+ this.readyCallbacks.push(callback);
4252
+ }
4253
+ }
4254
+ /**
4255
+ * Check if the SDK is fully initialized and ready.
4256
+ */
4257
+ isReady() {
4258
+ return this.isInitialized;
4259
+ }
4046
4260
  /**
4047
4261
  * Register a schema for event validation.
4048
4262
  * When debug mode is enabled, events will be validated against registered schemas.
@@ -4337,14 +4551,42 @@ if (typeof window !== 'undefined') {
4337
4551
  /**
4338
4552
  * Clianta SDK - React Integration
4339
4553
  *
4340
- * Provides CliantaProvider component for easy React/Next.js integration
4341
- * using the clianta.config.ts pattern.
4554
+ * Provides CliantaProvider component (with ErrorBoundary) for easy
4555
+ * React/Next.js integration using the clianta.config.ts pattern.
4556
+ */
4557
+ const CliantaContext = react.createContext({
4558
+ tracker: null,
4559
+ isReady: false,
4560
+ });
4561
+ /**
4562
+ * Internal ErrorBoundary to prevent SDK errors from crashing the host app.
4563
+ * Catches render-time errors in the provider tree.
4342
4564
  */
4343
- // Context for accessing tracker throughout the app
4344
- const CliantaContext = react.createContext(null);
4565
+ class CliantaErrorBoundary extends react.Component {
4566
+ constructor(props) {
4567
+ super(props);
4568
+ this.state = { hasError: false };
4569
+ }
4570
+ static getDerivedStateFromError() {
4571
+ return { hasError: true };
4572
+ }
4573
+ componentDidCatch(error, errorInfo) {
4574
+ console.error('[Clianta] SDK error caught by ErrorBoundary:', error);
4575
+ this.props.onError?.(error, errorInfo);
4576
+ }
4577
+ render() {
4578
+ if (this.state.hasError) {
4579
+ // Render children anyway — SDK failure shouldn't break the host UI
4580
+ return this.props.fallback ?? this.props.children;
4581
+ }
4582
+ return this.props.children;
4583
+ }
4584
+ }
4345
4585
  /**
4346
4586
  * CliantaProvider - Wrap your app to enable tracking
4347
4587
  *
4588
+ * Includes an ErrorBoundary so SDK failures never crash the host app.
4589
+ *
4348
4590
  * @example
4349
4591
  * // In clianta.config.ts:
4350
4592
  * import { CliantaConfig } from '@clianta/sdk';
@@ -4365,8 +4607,9 @@ const CliantaContext = react.createContext(null);
4365
4607
  * {children}
4366
4608
  * </CliantaProvider>
4367
4609
  */
4368
- function CliantaProvider({ config, children }) {
4610
+ function CliantaProvider({ config, children, onError }) {
4369
4611
  const [tracker, setTracker] = react.useState(null);
4612
+ const [isReady, setIsReady] = react.useState(false);
4370
4613
  // Stable ref to projectId — the only value that truly identifies the tracker
4371
4614
  const projectIdRef = react.useRef(config.projectId);
4372
4615
  react.useEffect(() => {
@@ -4380,18 +4623,29 @@ function CliantaProvider({ config, children }) {
4380
4623
  if (projectIdRef.current !== projectId) {
4381
4624
  projectIdRef.current = projectId;
4382
4625
  }
4383
- // Extract projectId (handled separately) and pass rest as options
4384
- const { projectId: _, ...options } = config;
4385
- const instance = clianta(projectId, options);
4386
- setTracker(instance);
4626
+ try {
4627
+ // Extract projectId (handled separately) and pass rest as options
4628
+ const { projectId: _, ...options } = config;
4629
+ const instance = clianta(projectId, options);
4630
+ setTracker(instance);
4631
+ setIsReady(true);
4632
+ }
4633
+ catch (error) {
4634
+ console.error('[Clianta] Failed to initialize SDK:', error);
4635
+ onError?.(error, { componentStack: '' });
4636
+ }
4387
4637
  // Cleanup: flush pending events on unmount
4388
4638
  return () => {
4389
- instance?.flush();
4639
+ tracker?.flush();
4640
+ setIsReady(false);
4390
4641
  };
4391
4642
  // eslint-disable-next-line react-hooks/exhaustive-deps
4392
4643
  }, [config.projectId]);
4393
- return (jsxRuntime.jsx(CliantaContext.Provider, { value: tracker, children: children }));
4644
+ return (jsxRuntime.jsx(CliantaErrorBoundary, { onError: onError, children: jsxRuntime.jsx(CliantaContext.Provider, { value: { tracker, isReady }, children: children }) }));
4394
4645
  }
4646
+ // ============================================
4647
+ // HOOKS
4648
+ // ============================================
4395
4649
  /**
4396
4650
  * useClianta - Hook to access tracker in any component
4397
4651
  *
@@ -4400,7 +4654,21 @@ function CliantaProvider({ config, children }) {
4400
4654
  * tracker?.track('button_click', 'CTA Button');
4401
4655
  */
4402
4656
  function useClianta() {
4403
- return react.useContext(CliantaContext);
4657
+ const { tracker } = react.useContext(CliantaContext);
4658
+ return tracker;
4659
+ }
4660
+ /**
4661
+ * useCliantaReady - Hook to check if SDK is initialized
4662
+ *
4663
+ * @example
4664
+ * const { isReady, tracker } = useCliantaReady();
4665
+ * if (isReady) {
4666
+ * tracker.track('purchase', 'Order', { value: 99 });
4667
+ * }
4668
+ */
4669
+ function useCliantaReady() {
4670
+ const { tracker, isReady } = react.useContext(CliantaContext);
4671
+ return { isReady, tracker };
4404
4672
  }
4405
4673
  /**
4406
4674
  * useCliantaTrack - Convenience hook for tracking events
@@ -4418,5 +4686,6 @@ function useCliantaTrack() {
4418
4686
 
4419
4687
  exports.CliantaProvider = CliantaProvider;
4420
4688
  exports.useClianta = useClianta;
4689
+ exports.useCliantaReady = useCliantaReady;
4421
4690
  exports.useCliantaTrack = useCliantaTrack;
4422
4691
  //# sourceMappingURL=react.cjs.js.map