saasco-sdk 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -33,9 +33,14 @@ import { Saasco } from 'saasco-sdk';
33
33
  export const saasco = new Saasco({ projectId: 'YOUR-PROJECT-ID' });
34
34
  ```
35
35
 
36
- ## Init Automatic Page View Tracking
36
+ ## Automatic Page View Tracking
37
+
38
+ When you initiate the lib Saasco will automatically start tracking all page views in your app. By default, it tracks all URL changes, including query parameters and hash changes. You can customize this behavior using the configuration options:
39
+
40
+ - `autoPageTracking` (default: `true`): When set to `false`, will not automatically track any `Page View` events.
41
+ - `trackUrlParams` (default: `true`): When set to `false`, changes to URL query parameters won't trigger a new page view
42
+ - `trackHashChanges` (default: `true`): When set to `false`, changes to URL hash won't trigger a new page view
37
43
 
38
- When you initiate the lib Saasco will automatically start tracking all page views in your app. There is no other configuration. It will automatically track url changes, even for SPAs like NextJs and Vue.
39
44
  You can also [manually track pages](https://www.notion.so/Manual-Page-Tracking-in-SPAs-2442fc7586dc4208ae8f669eb7561b1a?pvs=21) by opting out of automatic page tracking. For most use cases you don't need to do this.
40
45
 
41
46
  ```ts
@@ -168,7 +173,7 @@ saasco.debug(true);
168
173
  ## Reserved Properties
169
174
 
170
175
  Saasco has reserved some properties that have semantic meanings for contacts and, and will handle them in special ways.
171
- For example, Saasco always expects email to be a string of the users email address, this is important for when sending emails using the marketing app.
176
+ For example, Saasco always expects email to be a string of the user's email address, this is important for when sending emails using the marketing app.
172
177
  The SDK will do its best to match these properties, eg (created_at will be mapped to createdAt), but if possible you should use the reserved properties.
173
178
 
174
179
  ### Reserved Contact Properties
@@ -177,8 +182,8 @@ The SDK will do its best to match these properties, eg (created_at will be mappe
177
182
  | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------- |
178
183
  | age | Number | Age of a user |
179
184
  | avatar | String | URL to an avatar image for the user |
180
- | birthday | Date | Users birthday |
181
- | createdAt | Date | Date the users account was first created. We recomend recommend using ISO-8601 date strings. |
185
+ | birthday | Date | User's birthday |
186
+ | createdAt | Date | Date the user's account was first created. We recomend recommend using ISO-8601 date strings. |
182
187
  | description | String | Description of the user |
183
188
  | email | String | Email address of a user |
184
189
  | firstName | String | First name of a user |
@@ -187,8 +192,8 @@ The SDK will do its best to match these properties, eg (created_at will be mappe
187
192
  | lastName | String | Last name of a user |
188
193
  | name | String | Full name of a user. If you only pass a first and last name Segment automatically fills in the full name for you. |
189
194
  | phone | String | Phone number of a user |
190
- | title | String | Title of a user, usually related to their position at a specific company. Example: VP of Engineering |
191
- | username | String | Users username. This should be unique to each user, like the usernames of Twitter or GitHub. |
195
+ | title | String | Title of a user, usually related to their position at a specific company. Example: "VP of Engineering" |
196
+ | username | String | User's username. This should be unique to each user, like the usernames of Twitter or GitHub. |
192
197
  | website | String | Website of a user |
193
198
 
194
199
  ### Default Contact Properties
@@ -227,6 +232,13 @@ Any property that starts with `$` is a property that has been generated by the S
227
232
  | $initialUtmContent | Initial UTM Content | The initial UTM content tag from the URL a customer clicked to arrive at your domain. |
228
233
  | $utmContent | Last Touch UTM Content | The UTM content tag from the URL a customer clicked during their last interaction. |
229
234
  | $unsubscribed | Unsubscribed | Whether the user has unsubscribed from all notifications |
235
+ | $unsubscribeReason | Unsubscribe Reason | The reason the user was unsubscribed - eg complained, bounced, requested |
236
+
237
+ ### Reserved Integration Properties
238
+
239
+ | Property | Display Name | Description |
240
+ | ----------------- | ------------------ | ------------------------------------------------ |
241
+ | $stripeCustomerId | Stripe Customer Id | The Stripe customer ID associated with this user |
230
242
 
231
243
  ### Reserved event properties
232
244
 
package/index.cjs.js CHANGED
@@ -6,6 +6,8 @@ var tslib = require('tslib');
6
6
  var uuid = require('@lukeed/uuid');
7
7
  var zod = require('zod');
8
8
 
9
+ var version = "0.1.15";
10
+
9
11
  const timezones = {
10
12
  'Asia/Barnaul': 'RU',
11
13
  'Africa/Nouakchott': 'MR',
@@ -434,10 +436,39 @@ const timezones = {
434
436
  'Asia/Calcutta': 'IN'
435
437
  };
436
438
 
437
- var version = "0.1.13";
439
+ function getBrowserContext() {
440
+ var _a;
441
+ const isBrowser = typeof window !== 'undefined';
442
+ if (!isBrowser) return;
443
+ const customNavigator = navigator;
444
+ const $locale = (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages) && (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages.length) ? customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages[0] : (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.userLanguage) || (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.language) || (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.browserLanguage) || 'en';
445
+ // https://caniuse.com/?search=Intl.DateTimeFormat().resolvedOptions().timeZone
446
+ // Only has 96.63% global support, so we need to check for undefined
447
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
448
+ const $location = timezones[timezone] || ((_a = $locale === null || $locale === void 0 ? void 0 : $locale.split('-')) === null || _a === void 0 ? void 0 : _a[1]) || 'undefined';
449
+ const $href = window.location.href;
450
+ const $pathname = window.location.pathname;
451
+ const $referrer = document.referrer;
452
+ const $screenDPI = window.devicePixelRatio;
453
+ const $screenHeight = screen.height;
454
+ const $screenWidth = screen.width;
455
+ const $title = document.title;
456
+ const $userAgent = window.navigator.userAgent;
457
+ return {
458
+ $locale,
459
+ $location,
460
+ $href,
461
+ $pathname,
462
+ $referrer,
463
+ $screenDPI,
464
+ $screenHeight,
465
+ $screenWidth,
466
+ $title,
467
+ $userAgent
468
+ };
469
+ }
438
470
 
439
471
  const isBrowser = typeof window !== 'undefined';
440
- let userId = null;
441
472
  const PREF = 'saasco-sdk';
442
473
  const data = {};
443
474
  function storeData(key, value, ttl) {
@@ -503,35 +534,11 @@ function setAnonmousId({
503
534
  storeData(`anonymous-id`, anonymousId, 1000 * 60 * 60 * 24 * 365);
504
535
  return anonymousId;
505
536
  }
506
- function getBrowserContext() {
507
- var _a;
508
- if (!isBrowser) return {};
509
- const customNavigator = navigator;
510
- const $locale = (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages) && (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages.length) ? customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.languages[0] : (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.userLanguage) || (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.language) || (customNavigator === null || customNavigator === void 0 ? void 0 : customNavigator.browserLanguage) || 'en';
511
- // https://caniuse.com/?search=Intl.DateTimeFormat().resolvedOptions().timeZone
512
- // Only has 96.63% global support, so we need to check for undefined
513
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
514
- const $location = timezones[timezone] || ((_a = $locale === null || $locale === void 0 ? void 0 : $locale.split('-')) === null || _a === void 0 ? void 0 : _a[1]) || 'undefined';
515
- const $href = window.location.href;
516
- const $pathname = window.location.pathname;
517
- const $referrer = document.referrer;
518
- const $screenDPI = window.devicePixelRatio;
519
- const $screenHeight = screen.height;
520
- const $screenWidth = screen.width;
521
- const $title = document.title;
522
- const $userAgent = window.navigator.userAgent;
523
- return {
524
- $locale,
525
- $location,
526
- $href,
527
- $pathname,
528
- $referrer,
529
- $screenDPI,
530
- $screenHeight,
531
- $screenWidth,
532
- $title,
533
- $userAgent
534
- };
537
+ function getUserId() {
538
+ return retrieveData(`user-id`);
539
+ }
540
+ function setUserId(userId) {
541
+ storeData(`user-id`, userId, 1000 * 60 * 60 * 24 * 365);
535
542
  }
536
543
  class Saasco {
537
544
  /**
@@ -542,10 +549,12 @@ class Saasco {
542
549
  * @param config.autoPageTracking Whether to automatically track page views. Default is false.
543
550
  * @param config.enabled Whether analytics is enabled. Default is true. Set to false for development and staging envioronments. Will still allow debug mode to be true, just no events will be sent
544
551
  * @param config.debug Whether to log debug information. Default is false.
552
+ * @param config.trackUrlParams Whether to track URL parameters. Default is true.
553
+ * @param config.trackHashChanges Whether to track hash changes. Default is true.
545
554
  */
546
555
  constructor(config) {
547
556
  this.config = config;
548
- this.lastPageViewHref = '';
557
+ this.lastPageViewPath = '';
549
558
  this.isInitialized = false;
550
559
  if (!config.projectId) {
551
560
  this.error("Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
@@ -553,6 +562,9 @@ class Saasco {
553
562
  }
554
563
  // default enabled to true
555
564
  this.config.enabled = this.config.enabled === undefined ? true : this.config.enabled;
565
+ // default URL tracking options
566
+ this.config.trackUrlParams = this.config.trackUrlParams === undefined ? true : this.config.trackUrlParams;
567
+ this.config.trackHashChanges = this.config.trackHashChanges === undefined ? true : this.config.trackHashChanges;
556
568
  }
557
569
  init() {
558
570
  if (this.isInitialized) {
@@ -590,16 +602,6 @@ class Saasco {
590
602
  if (!this.config.projectId) {
591
603
  return this.error("Unable to track event. Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
592
604
  }
593
- // Prevent duplicate Page View tracking
594
- if (name === 'Page View') {
595
- if (!isBrowser) {
596
- console.warn('Saasco page tracking is only available in the browser');
597
- return;
598
- }
599
- const pageViewHref = window.location.href;
600
- if (pageViewHref === this.lastPageViewHref) return;
601
- this.lastPageViewHref = pageViewHref;
602
- }
603
605
  setSessionId();
604
606
  setAnonmousId();
605
607
  const browserContext = getBrowserContext();
@@ -610,9 +612,9 @@ class Saasco {
610
612
  version,
611
613
  sessionId: getSessionId(),
612
614
  anonymousId: getAnonymousId(),
613
- distinctId: userId,
615
+ distinctId: getUserId(),
614
616
  projectId: this.config.projectId,
615
- payload: JSON.stringify(Object.assign(Object.assign({}, browserContext), {
617
+ payload: JSON.stringify(Object.assign(Object.assign({}, browserContext || {}), {
616
618
  properties: properties || {}
617
619
  })),
618
620
  context: JSON.stringify(context || {})
@@ -626,6 +628,23 @@ class Saasco {
626
628
  * Before implementing this make sure you have disabled the autoPageTracking in the config or you will get duplicate page views
627
629
  */
628
630
  page() {
631
+ if (!isBrowser) {
632
+ console.warn('Saasco page tracking is only available in the browser');
633
+ return;
634
+ }
635
+ const url = new URL(window.location.href);
636
+ let pathToCompare = url.pathname;
637
+ // Include search params if configured
638
+ if (this.config.trackUrlParams) {
639
+ pathToCompare += url.search;
640
+ }
641
+ // Include hash if configured
642
+ if (this.config.trackHashChanges) {
643
+ pathToCompare += url.hash;
644
+ }
645
+ // Only track if the path has changed
646
+ if (pathToCompare === this.lastPageViewPath) return;
647
+ this.lastPageViewPath = pathToCompare;
629
648
  return this.track('Page View');
630
649
  }
631
650
  identify(distinctIdOrProperties, propertiesOrContext, contextOrNothing) {
@@ -639,7 +658,7 @@ class Saasco {
639
658
  // When the user gets identified as null this will reset the users session and anonymous id
640
659
  // This should only be called when distinctId is null and there is an existing userId.
641
660
  // This means the user has logged out and we should reset the session and anonymous IDs
642
- const userIdChangedToNull = distinctId === null && !!userId;
661
+ const userIdChangedToNull = distinctId === null && !!getUserId();
643
662
  if (userIdChangedToNull) this.log('User ID change to null. Session reset');
644
663
  const reset = userIdChangedToNull;
645
664
  setSessionId({
@@ -649,7 +668,7 @@ class Saasco {
649
668
  reset
650
669
  });
651
670
  // set the distinct Id to the userId
652
- userId = distinctId;
671
+ setUserId(distinctId);
653
672
  // No distinctId provided so we don't track the user
654
673
  if (!distinctId) return;
655
674
  const data = {
@@ -809,6 +828,9 @@ function coerceReservedProperties(properties) {
809
828
  }, {
810
829
  key: 'displayName',
811
830
  coerceFrom: ['display_name', 'displayName', 'user_display_name', 'userDisplayName', 'full_name', 'fullName', 'user_full_name', 'userFullName', 'complete_name', 'completeName', 'user_complete_name', 'userCompleteName', 'name', 'username']
831
+ }, {
832
+ key: '$stripeCustomerId',
833
+ coerceFrom: ['stripe_customer_id', 'stripeCustomerId', 'stripe_customer_id', 'stripeCustomerId', 'stripe_customer_id', 'stripeCustomerId', 'stripe_customer_id', 'stripeCustomerId', 'stripe_customer_id', 'stripeCustomerId']
812
834
  }];
813
835
  return coerce.reduce((acc, {
814
836
  key,
@@ -856,6 +878,61 @@ function coerceValue(properties, keys, schema) {
856
878
  return coercedValue;
857
879
  }
858
880
 
881
+ const browserContextSchema = zod.z.object({
882
+ $locale: zod.z.string(),
883
+ $location: zod.z.string(),
884
+ $href: zod.z.string(),
885
+ $pathname: zod.z.string(),
886
+ $referrer: zod.z.string(),
887
+ $screenDPI: zod.z.number(),
888
+ $screenHeight: zod.z.number(),
889
+ $screenWidth: zod.z.number(),
890
+ $title: zod.z.string(),
891
+ $userAgent: zod.z.string()
892
+ });
893
+ const serverContextSchema = zod.z.object({
894
+ $city: zod.z.string().optional(),
895
+ $country: zod.z.string().optional(),
896
+ $continent: zod.z.string().optional(),
897
+ $latitude: zod.z.number().optional(),
898
+ $longitude: zod.z.number().optional(),
899
+ $timezone: zod.z.string().optional(),
900
+ $userAgent: zod.z.string().optional(),
901
+ $referrer: zod.z.string().optional(),
902
+ $ip: zod.z.string().optional(),
903
+ $processedAt: zod.z.string().datetime().optional(),
904
+ $identityHash: zod.z.string().optional()
905
+ });
906
+ const trackedEventTypesSchema = zod.z.enum(['SHORTLINK_REDIRECT']);
907
+ const trackedEventSchema = zod.z.object({
908
+ id: zod.z.string().uuid(),
909
+ timestamp: zod.z.string().datetime(),
910
+ projectId: zod.z.string(),
911
+ version: zod.z.string(),
912
+ type: trackedEventTypesSchema,
913
+ payload: zod.z.object({
914
+ browserContext: browserContextSchema.optional(),
915
+ serverContext: serverContextSchema.optional(),
916
+ properties: zod.z.record(zod.z.string(), zod.z.any()).optional()
917
+ })
918
+ });
919
+ /**
920
+ * Creates a zod for tracking any type of event.
921
+ * Includes the base tracking schema and extends with any new schema
922
+ * @param type
923
+ * @param payloadSchema
924
+ * @returns
925
+ */
926
+ function createTrackedEventTypeSchema(type, payloadSchema) {
927
+ const typeSchema = zod.z.literal(type);
928
+ return trackedEventSchema.extend({
929
+ type: typeSchema,
930
+ payload: zod.z.object(Object.assign(Object.assign({}, trackedEventSchema.shape.payload.shape), {
931
+ properties: payloadSchema
932
+ }))
933
+ });
934
+ }
935
+
859
936
  const tbAny = zod.z.union([zod.z.string(), zod.z.number(), zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))]);
860
937
  const tinyBirdBaseSchema = zod.z.object({
861
938
  meta: zod.z.array(zod.z.object({
@@ -895,7 +972,9 @@ const TinybirdJobResponseSchema = zod.z.object({
895
972
  const latestEventsRequestSchema = zod.z.object({
896
973
  projectId: zod.z.string(),
897
974
  limit: zod.z.number().optional(),
898
- payload: zod.z.boolean().optional()
975
+ payload: zod.z.boolean().optional(),
976
+ distinctId: zod.z.string().optional(),
977
+ action: zod.z.string().optional()
899
978
  });
900
979
  const latestEventItemSchema = zod.z.object({
901
980
  id: zod.z.string(),
@@ -906,7 +985,9 @@ const latestEventItemSchema = zod.z.object({
906
985
  href: zod.z.string(),
907
986
  device: zod.z.string(),
908
987
  browser: zod.z.string(),
909
- payload: zod.z.string().optional()
988
+ payload: zod.z.string().optional(),
989
+ distinctId: zod.z.string().optional(),
990
+ unique: zod.z.boolean().optional()
910
991
  });
911
992
  const latestEventsResponseSchema = tinyBirdBaseSchema.extend({
912
993
  data: zod.z.array(latestEventItemSchema)
@@ -1018,7 +1099,7 @@ const currentUsersResponseSchema = tinyBirdBaseSchema.extend({
1018
1099
  }))
1019
1100
  });
1020
1101
  const reservedPropertiesSchema = zod.z.object({
1021
- age: zod.z.number().or(zod.z.string()).nullish(),
1102
+ age: zod.z.number().nullish(),
1022
1103
  avatar: zod.z.string().url().nullish(),
1023
1104
  birthday: zod.z.string().nullish(),
1024
1105
  createdAt: zod.z.string().nullish(),
@@ -1033,8 +1114,17 @@ const reservedPropertiesSchema = zod.z.object({
1033
1114
  title: zod.z.string().nullish(),
1034
1115
  username: zod.z.string().nullish(),
1035
1116
  website: zod.z.string().nullish(),
1117
+ // Default Properties
1036
1118
  $id: zod.z.string().nullish(),
1037
- $lastSeen: zod.z.string().nullish()
1119
+ $lastSeen: zod.z.string().nullish(),
1120
+ $unsubscribed: zod.z.boolean().nullish(),
1121
+ $unsubscribeReason: zod.z.enum(['manual', 'complained', 'bounced']).nullish(),
1122
+ // Stripe Properties
1123
+ $stripeCustomerId: zod.z.string().nullish(),
1124
+ $stripeTotalPayments: zod.z.number().or(zod.z.string()).nullish(),
1125
+ $stripeTotalSpent: zod.z.number().or(zod.z.string()).nullish(),
1126
+ // Internal properties
1127
+ $_toDelete: zod.z.boolean().nullish()
1038
1128
  });
1039
1129
  const contactPropertiesSchema = reservedPropertiesSchema.optional().and(zod.z.record(zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.date(), zod.z.null(), zod.z.array(zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.date(), zod.z.null()]))])));
1040
1130
  const analyticsUserRawSchema = zod.z.object({
@@ -1085,11 +1175,14 @@ exports.analyticsEventSchema = analyticsEventSchema;
1085
1175
  exports.analyticsUserRawSchema = analyticsUserRawSchema;
1086
1176
  exports.analyticsUserSchema = analyticsUserSchema;
1087
1177
  exports.baseTopRequestSchema = baseTopRequestSchema;
1178
+ exports.browserContextSchema = browserContextSchema;
1088
1179
  exports.coerceReservedProperties = coerceReservedProperties;
1089
1180
  exports.contactPropertiesSchema = contactPropertiesSchema;
1181
+ exports.createTrackedEventTypeSchema = createTrackedEventTypeSchema;
1090
1182
  exports.currentUsersRequestSchema = currentUsersRequestSchema;
1091
1183
  exports.currentUsersResponseSchema = currentUsersResponseSchema;
1092
1184
  exports.getAnalyticsUserParamsSchema = getAnalyticsUserParamsSchema;
1185
+ exports.getBrowserContext = getBrowserContext;
1093
1186
  exports.idenfitySchema = idenfitySchema;
1094
1187
  exports.kpiSchema = kpiSchema;
1095
1188
  exports.kpisRequestSchema = kpisRequestSchema;
@@ -1101,6 +1194,7 @@ exports.latestIdentifiesRequestSchema = latestIdentifiesRequestSchema;
1101
1194
  exports.latestIdentifiesResponseSchema = latestIdentifiesResponseSchema;
1102
1195
  exports.listUsersParamsSchema = listUsersParamsSchema;
1103
1196
  exports.reservedPropertiesSchema = reservedPropertiesSchema;
1197
+ exports.serverContextSchema = serverContextSchema;
1104
1198
  exports.tbAny = tbAny;
1105
1199
  exports.timezones = timezones;
1106
1200
  exports.tinyBirdBaseSchema = tinyBirdBaseSchema;
@@ -1111,4 +1205,6 @@ exports.topLocationsResponseSchema = topLocationsResponseSchema;
1111
1205
  exports.topPagesResponseSchema = topPagesResponseSchema;
1112
1206
  exports.topSourcesResponseSchema = topSourcesResponseSchema;
1113
1207
  exports.topSourcesSchema = topSourcesSchema;
1208
+ exports.trackedEventSchema = trackedEventSchema;
1209
+ exports.trackedEventTypesSchema = trackedEventTypesSchema;
1114
1210
  exports.userEventsResponseSchema = userEventsResponseSchema;