@reservamos/browser-analytics 0.1.1 → 0.1.4-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reservamos/browser-analytics",
3
- "version": "0.1.1",
3
+ "version": "0.1.4-alpha.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/reservamos/reservamos-browser-analytics.git"
@@ -0,0 +1,85 @@
1
+ import validatorService from '@/services/validator';
2
+ import fingerprintService from '../../services/fingerprint';
3
+ import mixpanelService from '../../services/mixpanel';
4
+ import { DefaultProperties } from './identifySchema';
5
+
6
+ const KNOWN_PROPERTIES = ['firstName', 'lastName', 'email', 'phone'];
7
+ const FINGERPRINT_PROPERTY = 'Known Fingerprints';
8
+
9
+ export interface UserProperties
10
+ extends DefaultProperties,
11
+ Record<string, string | number | boolean | undefined> {}
12
+
13
+ /**
14
+ * Maps user properties to a format compatible with Mixpanel's expected structure.
15
+ *
16
+ * This function takes a UserProperties object and transforms it into a Record
17
+ * where known properties are mapped to Mixpanel's specific keys (e.g., $first_name),
18
+ * and any additional properties are included as-is.
19
+ * @param {UserProperties} properties - The user properties to be mapped.
20
+ * @returns {Record<string, unknown>} A new object with mapped properties.
21
+ * @example
22
+ * const userProps = { firstName: 'John', lastName: 'Doe', age: 30 };
23
+ * const mappedProps = mapProperties(userProps);
24
+ * // Returns: { $first_name: 'John', $last_name: 'Doe', age: 30 }
25
+ */
26
+ function mapProperties(properties: UserProperties): Record<string, unknown> {
27
+ const parsedProperties = {
28
+ $first_name: properties?.firstName,
29
+ $last_name: properties?.lastName,
30
+ $email: properties?.email,
31
+ $phone: properties?.phone,
32
+ };
33
+
34
+ const additionalProperties = Object.keys(properties).reduce(
35
+ (acc, key) => {
36
+ if (!KNOWN_PROPERTIES.includes(key)) {
37
+ acc[key] = properties[key];
38
+ }
39
+ return acc;
40
+ },
41
+ {} as Record<string, unknown>,
42
+ );
43
+
44
+ return { ...parsedProperties, ...additionalProperties };
45
+ }
46
+
47
+ /**
48
+ * Identifies a user with given properties and fingerprint.
49
+ * @param {string} userId - The unique identifier for the user.
50
+ * @param {UserProperties} properties - Optional user properties.
51
+ * @throws {Error} If analytics service is not initialized or userId is missing.
52
+ * @returns {Promise<void>}
53
+ */
54
+ export async function identify(
55
+ userId: string,
56
+ properties: UserProperties = {},
57
+ ): Promise<void> {
58
+ if (!mixpanelService.isReady()) {
59
+ throw new Error('Analytics service is not initialized');
60
+ }
61
+
62
+ const validationResults = validatorService.parseIdentifyProps(properties);
63
+
64
+ if (validationResults.status === 'error') {
65
+ throw validationResults;
66
+ }
67
+
68
+ if (!userId) {
69
+ throw new Error('User ID is required');
70
+ }
71
+
72
+ const mappedProps = mapProperties(properties);
73
+
74
+ mixpanelService.identify(userId, mappedProps);
75
+
76
+ try {
77
+ const fingerprint = await fingerprintService.getFingerprint();
78
+
79
+ if (fingerprint) {
80
+ mixpanelService.attachProperty(FINGERPRINT_PROPERTY, fingerprint);
81
+ }
82
+ } catch (error) {
83
+ console.error('Error attaching fingerprint:', error);
84
+ }
85
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ const IdentifySchema = z.object({
4
+ firstName: z.string().optional(),
5
+ lastName: z.string().optional(),
6
+ email: z.string().email().optional(),
7
+ phone: z
8
+ .string()
9
+ .regex(/^\+?[1-9]\d{1,14}$/)
10
+ .optional(),
11
+ });
12
+
13
+ export type DefaultProperties = z.infer<typeof IdentifySchema>;
14
+
15
+ export default IdentifySchema;
@@ -0,0 +1,3 @@
1
+ import { identify } from './identify';
2
+
3
+ export default identify;
@@ -0,0 +1,3 @@
1
+ import trackTest from './trackTest';
2
+
3
+ export default trackTest;
@@ -1,10 +1,12 @@
1
- import { trackEvent } from '../track';
1
+ import { trackEvent } from '@/track';
2
2
 
3
3
  const EVENT_NAME = 'Track Test';
4
4
 
5
5
  /**
6
6
  First test event to track, it purpose is to test the tracking library and the identification service.ws
7
7
  */
8
- export function trackTest() {
8
+ function trackTest() {
9
9
  trackEvent(EVENT_NAME, {});
10
10
  }
11
+
12
+ export default trackTest;
package/src/index.ts CHANGED
@@ -1,12 +1,13 @@
1
- import { trackTest } from './events/trackTest';
2
- import { init } from './init';
1
+ import identify from '@/events/identify';
2
+ import trackTest from '@/events/test';
3
+ import { init } from '@/init';
3
4
 
4
- export { init } from './init';
5
- export { trackTest } from './events/trackTest';
6
-
7
- const tracker = {
5
+ const analytics = {
8
6
  init,
9
- trackTest,
7
+ identify,
8
+ track: {
9
+ test: trackTest,
10
+ },
10
11
  };
11
12
 
12
- export default tracker;
13
+ export default analytics;
package/src/init.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { z } from 'zod';
2
- import validator, {
3
- InitConfigSchema,
4
- } from './events/event_schema/validationSchema';
5
- import { initFingerprint } from './fingerprint'; // Import the new fingerprint initialization function
6
- import { initMixpanel, isMixpanelReady } from './mixpanel'; // Import Mixpanel functions
2
+ import fingerprintService from '@/services/fingerprint';
3
+ import mixpanelService from '@/services/mixpanel';
4
+ import validator, { InitConfigSchema } from './services/validator';
7
5
 
8
6
  /**
9
7
  * Configuration object for initializing the tracking library.
@@ -30,11 +28,11 @@ export async function init(config: InitConfig) {
30
28
 
31
29
  const { mixpanelToken, debug = false, identificationKey } = config;
32
30
 
33
- await initMixpanel(mixpanelToken, debug);
31
+ await mixpanelService.init(mixpanelToken, debug);
34
32
 
35
33
  // Only mixpanel is required to be ready to dispatch the 'Tracker Ready' event
36
34
  try {
37
- await initFingerprint(identificationKey);
35
+ await fingerprintService.initFingerprint(identificationKey);
38
36
  } catch (error) {
39
37
  console.error('Error initializing identification service:', error);
40
38
  }
@@ -50,5 +48,5 @@ export async function init(config: InitConfig) {
50
48
  * @returns {boolean} Returns true if the Mixpanel tracker is ready, otherwise false.
51
49
  */
52
50
  export function isTrackerReady(): boolean {
53
- return isMixpanelReady();
51
+ return mixpanelService.isReady();
54
52
  }
@@ -68,7 +68,7 @@ const setCachedFingerprint = (fingerprint: string): void => {
68
68
  * @returns {Promise<void>} A promise that resolves when initialization is complete.
69
69
  * @throws {Error} Throws an error if initialization fails.
70
70
  */
71
- export async function initFingerprint(apiKey: string): Promise<void> {
71
+ async function initFingerprint(apiKey: string): Promise<void> {
72
72
  window.fingerprintConfig = {};
73
73
  window.fpClient = new FpjsClient({
74
74
  loadOptions: {
@@ -90,7 +90,7 @@ export async function initFingerprint(apiKey: string): Promise<void> {
90
90
  * @returns {Promise<string>} A promise that resolves with the fingerprint.
91
91
  * @throws {Error} Throws an error if retrieval fails.
92
92
  */
93
- export async function getFingerprint(cacheOnly = false): Promise<string> {
93
+ async function getFingerprint(cacheOnly = false): Promise<string> {
94
94
  if (!window.fingerprintConfig) {
95
95
  throw new Error('Fingerprint configuration is not initialized.');
96
96
  }
@@ -126,6 +126,14 @@ export async function getFingerprint(cacheOnly = false): Promise<string> {
126
126
  * and is ready to use by checking if the `fpClient` instance exists on the window object.
127
127
  * @returns {boolean} Returns true if the identification service is ready, otherwise false.
128
128
  */
129
- export function isFingerprintReady(): boolean {
129
+ function isFingerprintReady(): boolean {
130
130
  return window.fpClient !== undefined;
131
131
  }
132
+
133
+ const fingerprintService = {
134
+ initFingerprint,
135
+ getFingerprint,
136
+ isFingerprintReady,
137
+ };
138
+
139
+ export default fingerprintService;
@@ -0,0 +1,89 @@
1
+ import mixpanel from 'mixpanel-browser';
2
+
3
+ declare global {
4
+ interface Window {
5
+ mixpanel: typeof mixpanel;
6
+ }
7
+ }
8
+
9
+ /**
10
+ * Initializes Mixpanel with the provided token and debug flag.
11
+ * @param {string} mixpanelToken - The Mixpanel token used for authenticating API requests.
12
+ * @param {boolean} debug - Optional flag to enable or disable debug mode.
13
+ * @returns {Promise<void>} A promise that resolves when Mixpanel is initialized.
14
+ */
15
+ function init(mixpanelToken: string, debug = false): Promise<void> {
16
+ return new Promise<void>((resolve) => {
17
+ mixpanel.init(mixpanelToken, {
18
+ debug,
19
+ loaded: () => {
20
+ window.mixpanel = mixpanel;
21
+ resolve();
22
+ },
23
+ });
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Identifies a user in Mixpanel and sets their properties.
29
+ * @param {string} userId - The unique identifier for the user.
30
+ * @param {Record<string, unknown>} properties - An object containing user properties to be set.
31
+ * @returns {void}
32
+ * @throws {Error} If Mixpanel is not initialized (handled internally).
33
+ */
34
+ function identify(userId: string, properties: Record<string, unknown>): void {
35
+ if (!isReady()) {
36
+ return;
37
+ }
38
+
39
+ mixpanel.identify(userId);
40
+ mixpanel.people.set(properties);
41
+ }
42
+
43
+ /**
44
+ * Appends a unique value to a property for the currently identified user in Mixpanel.
45
+ * If the property doesn't exist, it creates an array with the value.
46
+ * If the property exists, it adds the value only if it's not already present.
47
+ * @param {string} key - The name of the property to append to.
48
+ * @param {unknown} value - The value to append to the property.
49
+ * @throws {Error} If Mixpanel is not initialized (handled internally).
50
+ */
51
+ function attachProperty(key: string, value: unknown): void {
52
+ if (!isReady()) {
53
+ return;
54
+ }
55
+
56
+ mixpanel.people.union(key, [value]);
57
+ }
58
+
59
+ /**
60
+ * Checks if the Mixpanel tracker is ready.
61
+ * @returns {boolean} Returns true if the Mixpanel tracker is ready, otherwise false.
62
+ */
63
+ function isReady(): boolean {
64
+ return window.mixpanel !== undefined;
65
+ }
66
+ /**
67
+ * Tracks an event in Mixpanel with the given name and properties.
68
+ * @param {string} eventName - The name of the event to track.
69
+ * @param {Record<string, unknown>} properties - An object containing event properties.
70
+ * @returns {void}
71
+ * @throws {Error} If Mixpanel is not initialized (handled internally).
72
+ */
73
+ function track(eventName: string, properties: Record<string, unknown>): void {
74
+ if (!isReady()) {
75
+ return;
76
+ }
77
+
78
+ mixpanel.track(eventName, properties);
79
+ }
80
+
81
+ const mixpanelService = {
82
+ init,
83
+ isReady,
84
+ track,
85
+ identify,
86
+ attachProperty,
87
+ };
88
+
89
+ export default mixpanelService;
@@ -0,0 +1,161 @@
1
+ import { z, ZodError, ZodSchema } from 'zod';
2
+ import IdentifySchema from '@/events/identify/identifySchema';
3
+
4
+ const TrackTestEventSchema = z.object({}); // Allow empty object for track test event
5
+ export const InitConfigSchema = z.object({
6
+ /**
7
+ * The Mixpanel token used for authenticating API requests.
8
+ */
9
+ mixpanelToken: z.string().min(1, 'Mixpanel token is required'),
10
+ /**
11
+ * Optional flag to enable or disable debug mode.
12
+ * When set to true, additional debug information will be logged.
13
+ */
14
+ debug: z.boolean().optional(),
15
+ /**
16
+ * Key for tracking user identities.
17
+ */
18
+ identificationKey: z.string().min(1, 'Identification key is required'),
19
+ });
20
+ interface CustomError {
21
+ field: string;
22
+ error_type: string;
23
+ expected: string;
24
+ received: string;
25
+ message: string;
26
+ suggestion: string;
27
+ }
28
+ // Error formatting
29
+ const SchemaErrorFormatter = (error: ZodError): CustomError[] => {
30
+ return error.issues.map((issue) => {
31
+ console.log('issue==>', issue);
32
+ let error_type = 'INVALID_FIELD';
33
+ let expected = '';
34
+ let received = '';
35
+ let suggestion = '';
36
+
37
+ if (issue.code === 'invalid_type') {
38
+ error_type = 'TYPE_MISMATCH';
39
+ expected = issue.expected;
40
+ received = issue.received;
41
+ suggestion = `Expected ${expected} but received ${received}. Please provide a value of type ${expected}.`;
42
+ } else if (issue.code === 'too_small') {
43
+ error_type = 'VALUE_TOO_SMALL';
44
+ suggestion = `Increase the value to at least ${issue.minimum}.`;
45
+ } else if (issue.code === 'too_big') {
46
+ error_type = 'VALUE_TOO_BIG';
47
+ suggestion = `Reduce the value to no more than ${issue.maximum}.`;
48
+ } else {
49
+ error_type = 'INVALID_FIELD';
50
+ suggestion = `Ensure the field matches the expected format and value type.`;
51
+ }
52
+ return {
53
+ field: issue.path.join('.'),
54
+ error_type,
55
+ expected,
56
+ received,
57
+ message: issue.message,
58
+ suggestion,
59
+ };
60
+ });
61
+ };
62
+
63
+ // Mapping event names to Zod schemas
64
+ const eventSchemas: Record<string, z.ZodSchema> = {
65
+ 'Track Test': TrackTestEventSchema,
66
+ };
67
+
68
+ type EventData = z.infer<(typeof eventSchemas)[keyof typeof eventSchemas]>;
69
+
70
+ export interface ValidationResult {
71
+ status: 'ok' | 'error';
72
+ message: string;
73
+ errors?: CustomError[];
74
+ }
75
+
76
+ /**
77
+ * Validates the event data against the predefined schema for the given event name.
78
+ * @param {string} eventName - The name of the event to validate.
79
+ * @param {EventData} eventData - The data of the event to validate.
80
+ * @returns {ValidationResult} - The result of the validation, including status and message.
81
+ */
82
+ function parseEventProps(
83
+ eventName: string,
84
+ eventData: EventData,
85
+ ): ValidationResult {
86
+ const eventSchema = eventSchemas[eventName];
87
+
88
+ if (!eventSchema) {
89
+ return { status: 'error', message: `Event ${eventName} not found` };
90
+ }
91
+ // Validate schema normally if not an empty object
92
+ try {
93
+ eventSchema.parse(eventData);
94
+ return { status: 'ok', message: 'Schema is valid' };
95
+ } catch (error) {
96
+ if (error instanceof ZodError) {
97
+ const errors = SchemaErrorFormatter(error);
98
+ return {
99
+ status: 'error',
100
+ message: 'Schema validation failed',
101
+ errors,
102
+ };
103
+ }
104
+ return { status: 'error', message: 'Unknown validation error' };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Generic function to validate properties against a given schema.
110
+ * @param {object} properties - The properties to validate.
111
+ * @param {ZodSchema} schema - The schema to validate against.
112
+ * @returns {ValidationResult} - The result of the validation, including status and message.
113
+ */
114
+ function validateProps(
115
+ properties: object,
116
+ schema: ZodSchema,
117
+ ): ValidationResult {
118
+ try {
119
+ schema.parse(properties);
120
+ return { status: 'ok', message: 'Properties are valid' };
121
+ } catch (error) {
122
+ if (error instanceof ZodError) {
123
+ const errors = SchemaErrorFormatter(error);
124
+ return {
125
+ status: 'error',
126
+ message: 'Schema validation failed',
127
+ errors,
128
+ };
129
+ }
130
+ return { status: 'error', message: 'Unknown validation error' };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Validates the initialization properties.
136
+ * @param {object} initProps - The properties to validate for init.
137
+ * @returns {ValidationResult} - The result of the validation, including status and message.
138
+ */
139
+ function parseInitProps(initProps: object): ValidationResult {
140
+ return validateProps(initProps, InitConfigSchema);
141
+ }
142
+
143
+ /**
144
+ * Validates the properties for user identification.
145
+ * @param {Record<string, unknown>} properties - The properties to validate for user identification.
146
+ * @returns {ValidationResult} - The result of the validation, including status and message.
147
+ */
148
+ function parseIdentifyProps(
149
+ properties: Record<string, unknown>,
150
+ ): ValidationResult {
151
+ return validateProps(properties, IdentifySchema);
152
+ }
153
+
154
+ const validatorService = {
155
+ parseEventProps,
156
+ parseInitProps,
157
+ parseIdentifyProps,
158
+ };
159
+
160
+ // Export the validator object
161
+ export default validatorService;
package/src/track.ts CHANGED
@@ -1,7 +1,6 @@
1
- import mixpanel from 'mixpanel-browser';
2
- import validator from './events/event_schema/validationSchema';
3
- import { getFingerprint } from './fingerprint';
4
- import { isMixpanelReady } from './mixpanel';
1
+ import fingerprintService from '@/services/fingerprint';
2
+ import mixpanelService from '@/services/mixpanel';
3
+ import validator from '@/services/validator';
5
4
 
6
5
  /**
7
6
  * List of events that trigger the fingerprint to be sent with the event. other eventes will only fetch the cached fingerprint.
@@ -19,7 +18,7 @@ export async function trackEvent(
19
18
  eventName: string,
20
19
  eventProperties: object,
21
20
  ): Promise<void> {
22
- if (!isMixpanelReady()) {
21
+ if (!mixpanelService.isReady()) {
23
22
  throw new Error('Mixpanel is not initialized.');
24
23
  }
25
24
 
@@ -32,7 +31,7 @@ export async function trackEvent(
32
31
 
33
32
  const cacheOnly = !FP_TRIGGER_EVENTS.includes(eventName);
34
33
 
35
- const fingerprint = await getFingerprint(cacheOnly);
34
+ const fingerprint = await fingerprintService.getFingerprint(cacheOnly);
36
35
  const defaultProperties = {
37
36
  'User Fingerprint': fingerprint,
38
37
  };
@@ -42,7 +41,7 @@ export async function trackEvent(
42
41
  ...eventProperties,
43
42
  };
44
43
 
45
- mixpanel.track(eventName, properties);
44
+ mixpanelService.track(eventName, properties);
46
45
  } catch (error) {
47
46
  console.error('Error tracking event:', error);
48
47
  throw error;