@clianta/sdk 1.5.0 → 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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.5.0
2
+ * Clianta SDK v1.6.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -8,16 +8,23 @@
8
8
  * @see SDK_VERSION in core/config.ts
9
9
  */
10
10
  /** SDK Version */
11
- const SDK_VERSION = '1.4.0';
12
- /** Default API endpoint based on environment */
11
+ const SDK_VERSION = '1.6.0';
12
+ /** Default API endpoint reads from env or falls back to localhost */
13
13
  const getDefaultApiEndpoint = () => {
14
- if (typeof window === 'undefined')
15
- return 'https://api.clianta.online';
16
- const hostname = window.location.hostname;
17
- if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
18
- return 'http://localhost:5000';
14
+ // Build-time env var (works with Next.js, Vite, CRA, etc.)
15
+ if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_CLIANTA_API_ENDPOINT) {
16
+ return process.env.NEXT_PUBLIC_CLIANTA_API_ENDPOINT;
17
+ }
18
+ if (typeof process !== 'undefined' && process.env?.VITE_CLIANTA_API_ENDPOINT) {
19
+ return process.env.VITE_CLIANTA_API_ENDPOINT;
20
+ }
21
+ if (typeof process !== 'undefined' && process.env?.REACT_APP_CLIANTA_API_ENDPOINT) {
22
+ return process.env.REACT_APP_CLIANTA_API_ENDPOINT;
23
+ }
24
+ if (typeof process !== 'undefined' && process.env?.CLIANTA_API_ENDPOINT) {
25
+ return process.env.CLIANTA_API_ENDPOINT;
19
26
  }
20
- return 'https://api.clianta.online';
27
+ return 'http://localhost:5000';
21
28
  };
22
29
  /** Core plugins enabled by default */
23
30
  const DEFAULT_PLUGINS = [
@@ -1779,7 +1786,7 @@ class PopupFormsPlugin extends BasePlugin {
1779
1786
  return;
1780
1787
  const config = this.tracker.getConfig();
1781
1788
  const workspaceId = this.tracker.getWorkspaceId();
1782
- const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1789
+ const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
1783
1790
  try {
1784
1791
  const url = encodeURIComponent(window.location.href);
1785
1792
  const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
@@ -1884,7 +1891,7 @@ class PopupFormsPlugin extends BasePlugin {
1884
1891
  if (!this.tracker)
1885
1892
  return;
1886
1893
  const config = this.tracker.getConfig();
1887
- const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1894
+ const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
1888
1895
  try {
1889
1896
  await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
1890
1897
  method: 'POST',
@@ -2131,7 +2138,7 @@ class PopupFormsPlugin extends BasePlugin {
2131
2138
  if (!this.tracker)
2132
2139
  return;
2133
2140
  const config = this.tracker.getConfig();
2134
- const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
2141
+ const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
2135
2142
  const visitorId = this.tracker.getVisitorId();
2136
2143
  // Collect form data
2137
2144
  const formData = new FormData(formElement);
@@ -3026,7 +3033,7 @@ class CRMClient {
3026
3033
  * The contact is upserted in the CRM and matching workflow automations fire automatically.
3027
3034
  *
3028
3035
  * @example
3029
- * const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
3036
+ * const crm = new CRMClient('http://localhost:5000', 'WORKSPACE_ID');
3030
3037
  * crm.setApiKey('mm_live_...');
3031
3038
  *
3032
3039
  * await crm.sendEvent({
@@ -3683,6 +3690,85 @@ class CRMClient {
3683
3690
  }
3684
3691
  }
3685
3692
 
3693
+ /**
3694
+ * Privacy-safe visitor API client.
3695
+ * All methods return data for the current visitor only (no cross-visitor access).
3696
+ */
3697
+ class VisitorClient {
3698
+ constructor(transport, workspaceId, visitorId) {
3699
+ this.transport = transport;
3700
+ this.workspaceId = workspaceId;
3701
+ this.visitorId = visitorId;
3702
+ }
3703
+ /** Update visitorId (e.g. after reset) */
3704
+ setVisitorId(id) {
3705
+ this.visitorId = id;
3706
+ }
3707
+ basePath() {
3708
+ return `/api/public/track/visitor/${this.workspaceId}/${this.visitorId}`;
3709
+ }
3710
+ /**
3711
+ * Get the current visitor's profile from the CRM.
3712
+ * Returns visitor data and linked contact info if identified.
3713
+ */
3714
+ async getProfile() {
3715
+ const result = await this.transport.fetchData(`${this.basePath()}/profile`);
3716
+ if (result.success && result.data) {
3717
+ logger.debug('Visitor profile fetched:', result.data);
3718
+ return result.data;
3719
+ }
3720
+ logger.warn('Failed to fetch visitor profile:', result.error);
3721
+ return null;
3722
+ }
3723
+ /**
3724
+ * Get the current visitor's recent activity/events.
3725
+ * Returns paginated list of tracking events.
3726
+ */
3727
+ async getActivity(options) {
3728
+ const params = {};
3729
+ if (options?.page)
3730
+ params.page = options.page.toString();
3731
+ if (options?.limit)
3732
+ params.limit = options.limit.toString();
3733
+ if (options?.eventType)
3734
+ params.eventType = options.eventType;
3735
+ if (options?.startDate)
3736
+ params.startDate = options.startDate;
3737
+ if (options?.endDate)
3738
+ params.endDate = options.endDate;
3739
+ const result = await this.transport.fetchData(`${this.basePath()}/activity`, params);
3740
+ if (result.success && result.data) {
3741
+ return result.data;
3742
+ }
3743
+ logger.warn('Failed to fetch visitor activity:', result.error);
3744
+ return null;
3745
+ }
3746
+ /**
3747
+ * Get a summarized journey timeline for the current visitor.
3748
+ * Includes top pages, sessions, time spent, and recent activities.
3749
+ */
3750
+ async getTimeline() {
3751
+ const result = await this.transport.fetchData(`${this.basePath()}/timeline`);
3752
+ if (result.success && result.data) {
3753
+ return result.data;
3754
+ }
3755
+ logger.warn('Failed to fetch visitor timeline:', result.error);
3756
+ return null;
3757
+ }
3758
+ /**
3759
+ * Get engagement metrics for the current visitor.
3760
+ * Includes time on site, page views, bounce rate, and engagement score.
3761
+ */
3762
+ async getEngagement() {
3763
+ const result = await this.transport.fetchData(`${this.basePath()}/engagement`);
3764
+ if (result.success && result.data) {
3765
+ return result.data;
3766
+ }
3767
+ logger.warn('Failed to fetch visitor engagement:', result.error);
3768
+ return null;
3769
+ }
3770
+ }
3771
+
3686
3772
  /**
3687
3773
  * Clianta SDK - Main Tracker Class
3688
3774
  * @see SDK_VERSION in core/config.ts
@@ -3696,10 +3782,16 @@ class Tracker {
3696
3782
  this.isInitialized = false;
3697
3783
  /** contactId after a successful identify() call */
3698
3784
  this.contactId = null;
3785
+ /** groupId after a successful group() call */
3786
+ this.groupId = null;
3699
3787
  /** Pending identify retry on next flush */
3700
3788
  this.pendingIdentify = null;
3701
3789
  /** Registered event schemas for validation */
3702
3790
  this.eventSchemas = new Map();
3791
+ /** Event middleware pipeline */
3792
+ this.middlewares = [];
3793
+ /** Ready callbacks */
3794
+ this.readyCallbacks = [];
3703
3795
  if (!workspaceId) {
3704
3796
  throw new Error('[Clianta] Workspace ID is required');
3705
3797
  }
@@ -3725,6 +3817,8 @@ class Tracker {
3725
3817
  this.visitorId = this.createVisitorId();
3726
3818
  this.sessionId = this.createSessionId();
3727
3819
  logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3820
+ // Initialize visitor API client
3821
+ this.visitor = new VisitorClient(this.transport, this.workspaceId, this.visitorId);
3728
3822
  // Security warnings
3729
3823
  if (this.config.apiEndpoint.startsWith('http://') &&
3730
3824
  typeof window !== 'undefined' &&
@@ -3739,6 +3833,16 @@ class Tracker {
3739
3833
  this.initPlugins();
3740
3834
  this.isInitialized = true;
3741
3835
  logger.info('SDK initialized successfully');
3836
+ // Fire ready callbacks
3837
+ for (const cb of this.readyCallbacks) {
3838
+ try {
3839
+ cb();
3840
+ }
3841
+ catch (e) {
3842
+ logger.error('onReady callback error:', e);
3843
+ }
3844
+ }
3845
+ this.readyCallbacks = [];
3742
3846
  }
3743
3847
  /**
3744
3848
  * Create visitor ID based on storage mode
@@ -3851,6 +3955,10 @@ class Tracker {
3851
3955
  if (this.contactId) {
3852
3956
  event.contactId = this.contactId;
3853
3957
  }
3958
+ // Attach groupId if known (from a prior group() call)
3959
+ if (this.groupId) {
3960
+ event.groupId = this.groupId;
3961
+ }
3854
3962
  // Validate event against registered schema (debug mode only)
3855
3963
  this.validateEventSchema(eventType, properties);
3856
3964
  // Check consent before tracking
@@ -3864,8 +3972,11 @@ class Tracker {
3864
3972
  logger.debug('Event dropped (no consent):', eventName);
3865
3973
  return;
3866
3974
  }
3867
- this.queue.push(event);
3868
- logger.debug('Event tracked:', eventName, properties);
3975
+ // Run event through middleware pipeline
3976
+ this.runMiddleware(event, () => {
3977
+ this.queue.push(event);
3978
+ logger.debug('Event tracked:', eventName, properties);
3979
+ });
3869
3980
  }
3870
3981
  /**
3871
3982
  * Track a page view
@@ -3927,80 +4038,47 @@ class Tracker {
3927
4038
  }
3928
4039
  /**
3929
4040
  * Get the current visitor's profile from the CRM.
3930
- * Returns visitor data and linked contact info if identified.
3931
- * Only returns data for the current visitor (privacy-safe for frontend).
4041
+ * @deprecated Use `tracker.visitor.getProfile()` instead.
3932
4042
  */
3933
4043
  async getVisitorProfile() {
3934
4044
  if (!this.isInitialized) {
3935
4045
  logger.warn('SDK not initialized');
3936
4046
  return null;
3937
4047
  }
3938
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3939
- if (result.success && result.data) {
3940
- logger.debug('Visitor profile fetched:', result.data);
3941
- return result.data;
3942
- }
3943
- logger.warn('Failed to fetch visitor profile:', result.error);
3944
- return null;
4048
+ return this.visitor.getProfile();
3945
4049
  }
3946
4050
  /**
3947
4051
  * Get the current visitor's recent activity/events.
3948
- * Returns paginated list of tracking events for this visitor.
4052
+ * @deprecated Use `tracker.visitor.getActivity()` instead.
3949
4053
  */
3950
4054
  async getVisitorActivity(options) {
3951
4055
  if (!this.isInitialized) {
3952
4056
  logger.warn('SDK not initialized');
3953
4057
  return null;
3954
4058
  }
3955
- const params = {};
3956
- if (options?.page)
3957
- params.page = options.page.toString();
3958
- if (options?.limit)
3959
- params.limit = options.limit.toString();
3960
- if (options?.eventType)
3961
- params.eventType = options.eventType;
3962
- if (options?.startDate)
3963
- params.startDate = options.startDate;
3964
- if (options?.endDate)
3965
- params.endDate = options.endDate;
3966
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3967
- if (result.success && result.data) {
3968
- return result.data;
3969
- }
3970
- logger.warn('Failed to fetch visitor activity:', result.error);
3971
- return null;
4059
+ return this.visitor.getActivity(options);
3972
4060
  }
3973
4061
  /**
3974
4062
  * Get a summarized journey timeline for the current visitor.
3975
- * Includes top pages, sessions, time spent, and recent activities.
4063
+ * @deprecated Use `tracker.visitor.getTimeline()` instead.
3976
4064
  */
3977
4065
  async getVisitorTimeline() {
3978
4066
  if (!this.isInitialized) {
3979
4067
  logger.warn('SDK not initialized');
3980
4068
  return null;
3981
4069
  }
3982
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3983
- if (result.success && result.data) {
3984
- return result.data;
3985
- }
3986
- logger.warn('Failed to fetch visitor timeline:', result.error);
3987
- return null;
4070
+ return this.visitor.getTimeline();
3988
4071
  }
3989
4072
  /**
3990
4073
  * Get engagement metrics for the current visitor.
3991
- * Includes time on site, page views, bounce rate, and engagement score.
4074
+ * @deprecated Use `tracker.visitor.getEngagement()` instead.
3992
4075
  */
3993
4076
  async getVisitorEngagement() {
3994
4077
  if (!this.isInitialized) {
3995
4078
  logger.warn('SDK not initialized');
3996
4079
  return null;
3997
4080
  }
3998
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
3999
- if (result.success && result.data) {
4000
- return result.data;
4001
- }
4002
- logger.warn('Failed to fetch visitor engagement:', result.error);
4003
- return null;
4081
+ return this.visitor.getEngagement();
4004
4082
  }
4005
4083
  /**
4006
4084
  * Retry pending identify call
@@ -4031,6 +4109,149 @@ class Tracker {
4031
4109
  logger.enabled = enabled;
4032
4110
  logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
4033
4111
  }
4112
+ // ============================================
4113
+ // GROUP, ALIAS, SCREEN
4114
+ // ============================================
4115
+ /**
4116
+ * Associate the current visitor with a group (company/account).
4117
+ * The groupId will be attached to all subsequent track() calls.
4118
+ */
4119
+ group(groupId, traits = {}) {
4120
+ if (!groupId) {
4121
+ logger.warn('groupId is required for group()');
4122
+ return;
4123
+ }
4124
+ this.groupId = groupId;
4125
+ logger.info('Visitor grouped:', groupId);
4126
+ this.track('group', 'Group Identified', {
4127
+ groupId,
4128
+ ...traits,
4129
+ });
4130
+ }
4131
+ /**
4132
+ * Merge two visitor identities.
4133
+ * Links `previousId` (typically the anonymous visitor) to `newId` (the known user).
4134
+ * If `previousId` is omitted, the current visitorId is used.
4135
+ */
4136
+ async alias(newId, previousId) {
4137
+ if (!newId) {
4138
+ logger.warn('newId is required for alias()');
4139
+ return false;
4140
+ }
4141
+ const prevId = previousId || this.visitorId;
4142
+ logger.info('Aliasing visitor:', { from: prevId, to: newId });
4143
+ try {
4144
+ const url = `${this.config.apiEndpoint}/api/public/track/alias`;
4145
+ const response = await fetch(url, {
4146
+ method: 'POST',
4147
+ headers: { 'Content-Type': 'application/json' },
4148
+ body: JSON.stringify({
4149
+ workspaceId: this.workspaceId,
4150
+ previousId: prevId,
4151
+ newId,
4152
+ }),
4153
+ });
4154
+ if (response.ok) {
4155
+ logger.info('Alias successful');
4156
+ return true;
4157
+ }
4158
+ logger.error('Alias failed:', response.status);
4159
+ return false;
4160
+ }
4161
+ catch (error) {
4162
+ logger.error('Alias request failed:', error);
4163
+ return false;
4164
+ }
4165
+ }
4166
+ /**
4167
+ * Track a screen view (for mobile-first PWAs and SPAs).
4168
+ * Similar to page() but semantically for app screens.
4169
+ */
4170
+ screen(name, properties = {}) {
4171
+ this.track('screen_view', name, {
4172
+ ...properties,
4173
+ screenName: name,
4174
+ });
4175
+ }
4176
+ // ============================================
4177
+ // MIDDLEWARE
4178
+ // ============================================
4179
+ /**
4180
+ * Register event middleware.
4181
+ * Middleware functions receive the event and a `next` callback.
4182
+ * Call `next()` to pass the event through, or don't call it to drop the event.
4183
+ *
4184
+ * @example
4185
+ * tracker.use((event, next) => {
4186
+ * // Strip PII from events
4187
+ * delete event.properties.email;
4188
+ * next(); // pass it through
4189
+ * });
4190
+ */
4191
+ use(middleware) {
4192
+ this.middlewares.push(middleware);
4193
+ logger.debug('Middleware registered');
4194
+ }
4195
+ /**
4196
+ * Run event through the middleware pipeline.
4197
+ * Executes each middleware in order; if any skips `next()`, the event is dropped.
4198
+ */
4199
+ runMiddleware(event, finalCallback) {
4200
+ if (this.middlewares.length === 0) {
4201
+ finalCallback();
4202
+ return;
4203
+ }
4204
+ let index = 0;
4205
+ const middlewares = this.middlewares;
4206
+ const next = () => {
4207
+ index++;
4208
+ if (index < middlewares.length) {
4209
+ try {
4210
+ middlewares[index](event, next);
4211
+ }
4212
+ catch (e) {
4213
+ logger.error('Middleware error:', e);
4214
+ finalCallback();
4215
+ }
4216
+ }
4217
+ else {
4218
+ finalCallback();
4219
+ }
4220
+ };
4221
+ try {
4222
+ middlewares[0](event, next);
4223
+ }
4224
+ catch (e) {
4225
+ logger.error('Middleware error:', e);
4226
+ finalCallback();
4227
+ }
4228
+ }
4229
+ // ============================================
4230
+ // LIFECYCLE
4231
+ // ============================================
4232
+ /**
4233
+ * Register a callback to be invoked when the SDK is fully initialized.
4234
+ * If already initialized, the callback fires immediately.
4235
+ */
4236
+ onReady(callback) {
4237
+ if (this.isInitialized) {
4238
+ try {
4239
+ callback();
4240
+ }
4241
+ catch (e) {
4242
+ logger.error('onReady callback error:', e);
4243
+ }
4244
+ }
4245
+ else {
4246
+ this.readyCallbacks.push(callback);
4247
+ }
4248
+ }
4249
+ /**
4250
+ * Check if the SDK is fully initialized and ready.
4251
+ */
4252
+ isReady() {
4253
+ return this.isInitialized;
4254
+ }
4034
4255
  /**
4035
4256
  * Register a schema for event validation.
4036
4257
  * When debug mode is enabled, events will be validated against registered schemas.
@@ -4166,6 +4387,86 @@ class Tracker {
4166
4387
  this.sessionId = this.createSessionId();
4167
4388
  logger.info('All user data deleted');
4168
4389
  }
4390
+ // ============================================
4391
+ // PUBLIC CRM METHODS (no API key required)
4392
+ // ============================================
4393
+ /**
4394
+ * Create or update a contact by email (upsert).
4395
+ * Secured by domain whitelist — no API key needed.
4396
+ */
4397
+ async createContact(data) {
4398
+ return this.publicCrmRequest('/api/public/crm/contacts', 'POST', {
4399
+ workspaceId: this.workspaceId,
4400
+ ...data,
4401
+ });
4402
+ }
4403
+ /**
4404
+ * Update an existing contact by ID (limited fields only).
4405
+ */
4406
+ async updateContact(contactId, data) {
4407
+ return this.publicCrmRequest(`/api/public/crm/contacts/${contactId}`, 'PUT', {
4408
+ workspaceId: this.workspaceId,
4409
+ ...data,
4410
+ });
4411
+ }
4412
+ /**
4413
+ * Submit a form — creates/updates contact from form data.
4414
+ */
4415
+ async submitForm(formId, data) {
4416
+ const payload = {
4417
+ ...data,
4418
+ metadata: {
4419
+ ...data.metadata,
4420
+ visitorId: this.visitorId,
4421
+ sessionId: this.sessionId,
4422
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
4423
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
4424
+ },
4425
+ };
4426
+ return this.publicCrmRequest(`/api/public/crm/forms/${formId}/submit`, 'POST', payload);
4427
+ }
4428
+ /**
4429
+ * Log an activity linked to a contact (append-only).
4430
+ */
4431
+ async logActivity(data) {
4432
+ return this.publicCrmRequest('/api/public/crm/activities', 'POST', {
4433
+ workspaceId: this.workspaceId,
4434
+ ...data,
4435
+ });
4436
+ }
4437
+ /**
4438
+ * Create an opportunity (e.g., from "Request Demo" forms).
4439
+ */
4440
+ async createOpportunity(data) {
4441
+ return this.publicCrmRequest('/api/public/crm/opportunities', 'POST', {
4442
+ workspaceId: this.workspaceId,
4443
+ ...data,
4444
+ });
4445
+ }
4446
+ /**
4447
+ * Internal helper for public CRM API calls.
4448
+ */
4449
+ async publicCrmRequest(path, method, body) {
4450
+ const url = `${this.config.apiEndpoint}${path}`;
4451
+ try {
4452
+ const response = await fetch(url, {
4453
+ method,
4454
+ headers: { 'Content-Type': 'application/json' },
4455
+ body: JSON.stringify(body),
4456
+ });
4457
+ const data = await response.json().catch(() => ({}));
4458
+ if (response.ok) {
4459
+ logger.debug(`Public CRM ${method} ${path} succeeded`);
4460
+ return { success: true, data: data.data ?? data, status: response.status };
4461
+ }
4462
+ logger.error(`Public CRM ${method} ${path} failed (${response.status}):`, data.message);
4463
+ return { success: false, error: data.message, status: response.status };
4464
+ }
4465
+ catch (error) {
4466
+ logger.error(`Public CRM ${method} ${path} error:`, error);
4467
+ return { success: false, error: error.message };
4468
+ }
4469
+ }
4169
4470
  /**
4170
4471
  * Destroy tracker and cleanup
4171
4472
  */
@@ -4290,7 +4591,7 @@ if (typeof window !== 'undefined') {
4290
4591
  * @example
4291
4592
  * const instance = createCliantaTracker({
4292
4593
  * projectId: 'your-project-id',
4293
- * apiEndpoint: 'https://api.clianta.online',
4594
+ * apiEndpoint: environment.cliantaApiEndpoint || 'http://localhost:5000',
4294
4595
  * });
4295
4596
  *
4296
4597
  * instance.tracker?.track('page_view', 'Home Page');