@formo/analytics 0.1.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.
Files changed (91) hide show
  1. package/README.md +141 -0
  2. package/dist/cjs/src/FormoAnalytics.d.ts +30 -0
  3. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -0
  4. package/dist/cjs/src/FormoAnalytics.js +297 -0
  5. package/dist/cjs/src/FormoAnalytics.js.map +1 -0
  6. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +7 -0
  7. package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -0
  8. package/dist/cjs/src/FormoAnalyticsProvider.js +45 -0
  9. package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -0
  10. package/dist/cjs/src/constants/config.d.ts +430 -0
  11. package/dist/cjs/src/constants/config.d.ts.map +1 -0
  12. package/dist/cjs/src/constants/config.js +433 -0
  13. package/dist/cjs/src/constants/config.js.map +1 -0
  14. package/dist/cjs/src/constants/index.d.ts +2 -0
  15. package/dist/cjs/src/constants/index.d.ts.map +1 -0
  16. package/dist/cjs/src/constants/index.js +18 -0
  17. package/dist/cjs/src/constants/index.js.map +1 -0
  18. package/dist/cjs/src/index.d.ts +4 -0
  19. package/dist/cjs/src/index.d.ts.map +1 -0
  20. package/dist/cjs/src/index.js +20 -0
  21. package/dist/cjs/src/index.js.map +1 -0
  22. package/dist/cjs/src/types/base.d.ts +8 -0
  23. package/dist/cjs/src/types/base.d.ts.map +1 -0
  24. package/dist/cjs/src/types/base.js +3 -0
  25. package/dist/cjs/src/types/base.js.map +1 -0
  26. package/dist/cjs/src/types/index.d.ts +2 -0
  27. package/dist/cjs/src/types/index.d.ts.map +1 -0
  28. package/dist/cjs/src/types/index.js +18 -0
  29. package/dist/cjs/src/types/index.js.map +1 -0
  30. package/dist/cjs/src/utils/index.d.ts +2 -0
  31. package/dist/cjs/src/utils/index.d.ts.map +1 -0
  32. package/dist/cjs/src/utils/index.js +18 -0
  33. package/dist/cjs/src/utils/index.js.map +1 -0
  34. package/dist/cjs/src/utils/isNotEmptyObject.d.ts +2 -0
  35. package/dist/cjs/src/utils/isNotEmptyObject.d.ts.map +1 -0
  36. package/dist/cjs/src/utils/isNotEmptyObject.js +10 -0
  37. package/dist/cjs/src/utils/isNotEmptyObject.js.map +1 -0
  38. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  39. package/dist/esm/src/FormoAnalytics.d.ts +30 -0
  40. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -0
  41. package/dist/esm/src/FormoAnalytics.js +291 -0
  42. package/dist/esm/src/FormoAnalytics.js.map +1 -0
  43. package/dist/esm/src/FormoAnalyticsProvider.d.ts +7 -0
  44. package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -0
  45. package/dist/esm/src/FormoAnalyticsProvider.js +40 -0
  46. package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -0
  47. package/dist/esm/src/constants/config.d.ts +430 -0
  48. package/dist/esm/src/constants/config.d.ts.map +1 -0
  49. package/dist/esm/src/constants/config.js +430 -0
  50. package/dist/esm/src/constants/config.js.map +1 -0
  51. package/dist/esm/src/constants/index.d.ts +2 -0
  52. package/dist/esm/src/constants/index.d.ts.map +1 -0
  53. package/dist/esm/src/constants/index.js +2 -0
  54. package/dist/esm/src/constants/index.js.map +1 -0
  55. package/dist/esm/src/index.d.ts +4 -0
  56. package/dist/esm/src/index.d.ts.map +1 -0
  57. package/dist/esm/src/index.js +4 -0
  58. package/dist/esm/src/index.js.map +1 -0
  59. package/dist/esm/src/types/base.d.ts +8 -0
  60. package/dist/esm/src/types/base.d.ts.map +1 -0
  61. package/dist/esm/src/types/base.js +2 -0
  62. package/dist/esm/src/types/base.js.map +1 -0
  63. package/dist/esm/src/types/index.d.ts +2 -0
  64. package/dist/esm/src/types/index.d.ts.map +1 -0
  65. package/dist/esm/src/types/index.js +2 -0
  66. package/dist/esm/src/types/index.js.map +1 -0
  67. package/dist/esm/src/utils/index.d.ts +2 -0
  68. package/dist/esm/src/utils/index.d.ts.map +1 -0
  69. package/dist/esm/src/utils/index.js +2 -0
  70. package/dist/esm/src/utils/index.js.map +1 -0
  71. package/dist/esm/src/utils/isNotEmptyObject.d.ts +2 -0
  72. package/dist/esm/src/utils/isNotEmptyObject.d.ts.map +1 -0
  73. package/dist/esm/src/utils/isNotEmptyObject.js +6 -0
  74. package/dist/esm/src/utils/isNotEmptyObject.js.map +1 -0
  75. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  76. package/dist/index.umd.min.js +3 -0
  77. package/dist/index.umd.min.js.LICENSE.txt +19 -0
  78. package/dist/index.umd.min.js.map +1 -0
  79. package/package.json +89 -0
  80. package/src/FormoAnalytics.ts +264 -0
  81. package/src/FormoAnalyticsProvider.tsx +54 -0
  82. package/src/constants/config.ts +429 -0
  83. package/src/constants/index.ts +1 -0
  84. package/src/global.d.ts +8 -0
  85. package/src/index.ts +3 -0
  86. package/src/types/base.ts +6 -0
  87. package/src/types/index.ts +1 -0
  88. package/src/utils/index.ts +1 -0
  89. package/src/utils/isNotEmptyObject.ts +5 -0
  90. package/tsconfig.json +28 -0
  91. package/webpack.config.ts +23 -0
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@formo/analytics",
3
+ "version": "0.1.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/getformo/sdk.git"
7
+ },
8
+ "main": "dist/cjs/src/index.js",
9
+ "types": "dist/esm/src/index.d.ts",
10
+ "module": "dist/esm/src/index.js",
11
+ "unpkg": "dist/index.umd.min.js",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/esm/src/index.js",
15
+ "require": "./dist/cjs/src/index.js"
16
+ }
17
+ },
18
+ "private": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "axios": "^1.7.7"
25
+ },
26
+ "devDependencies": {
27
+ "@babel/core": "^7.x",
28
+ "@babel/plugin-syntax-flow": "^7.14.5",
29
+ "@babel/plugin-transform-react-jsx": "^7.14.9",
30
+ "@commitlint/cli": "^17.3.0",
31
+ "@commitlint/config-conventional": "^17.3.0",
32
+ "@semantic-release/github": "^8.0.7",
33
+ "@testing-library/react": "^13.4.0",
34
+ "@types/chai": "^4.3.1",
35
+ "@types/jsdom": "^20.0.1",
36
+ "@types/mocha": "^9.1.1",
37
+ "@types/node": "^18.11.9",
38
+ "@types/react": "^18.0.25",
39
+ "@types/sinon": "^10.0.12",
40
+ "@types/sinon-chai": "^3.2.9",
41
+ "@typescript-eslint/eslint-plugin": "^5.30.4",
42
+ "@typescript-eslint/parser": "^5.30.4",
43
+ "chai": "^4.3.6",
44
+ "commitizen": "^4.2.5",
45
+ "cz-conventional-changelog": "3.3.0",
46
+ "eslint": "^8.19.0",
47
+ "eslint-config-react-app": "^7.0.1",
48
+ "global-jsdom": "^8.6.0",
49
+ "husky": "^8.0.0",
50
+ "jsdom": "^21.1.0",
51
+ "mocha": "^10.0.0",
52
+ "nodemon": "^2.0.20",
53
+ "nyc": "^15.1.0",
54
+ "prettier": "^2.6.1",
55
+ "react": "^18.3.1",
56
+ "react-dom": "^18.3.1",
57
+ "semantic-release": "^19.0.5",
58
+ "semantic-release-export-data": "^1.0.1",
59
+ "sinon": "^14.0.0",
60
+ "sinon-chai": "^3.7.0",
61
+ "ts-loader": "^9.3.1",
62
+ "ts-node": "^10.8.2",
63
+ "typescript": "~4.8.0",
64
+ "webpack": "^5.74.0",
65
+ "webpack-cli": "^4.10.0"
66
+ },
67
+ "scripts": {
68
+ "prebuild": "yarn clean",
69
+ "publish": "npm version && npm publish",
70
+ "build": "yarn build-cjs && yarn build-esm && yarn webpack --mode=production",
71
+ "build-cjs": "yarn tsc --build",
72
+ "build-esm": "yarn tsc -m es6 --outdir dist/esm",
73
+ "clean": "rm -rf dist",
74
+ "lint": "eslint '{src,test}/**/*.{ts,tsx}'",
75
+ "test": "nyc mocha",
76
+ "test-watch": "nodemon --config test.nodemon.json",
77
+ "prepare": "husky install",
78
+ "commit": "git add . && cz"
79
+ },
80
+ "peerDependencies": {
81
+ "@types/react": ">=16.14.34",
82
+ "react": ">=16.14.0"
83
+ },
84
+ "config": {
85
+ "commitizen": {
86
+ "path": "./node_modules/cz-conventional-changelog"
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,264 @@
1
+ import axios from 'axios';
2
+ import { COUNTRY_LIST, EVENTS_API, SESSION_STORAGE_ID_KEY } from './constants';
3
+ import { isNotEmpty } from './utils';
4
+
5
+ interface IFormoAnalytics {
6
+ init(apiKey: string, projectId: string): Promise<FormoAnalytics>;
7
+ identify(userData: any): void;
8
+ page(): void;
9
+ track(eventName: string, eventData: any): void;
10
+ }
11
+ export class FormoAnalytics implements IFormoAnalytics {
12
+ private config: any;
13
+ private sessionIdKey: string = SESSION_STORAGE_ID_KEY;
14
+ private timezoneToCountry: Record<string, string> = COUNTRY_LIST;
15
+
16
+ private constructor(
17
+ public readonly apiKey: string,
18
+ public projectId: string
19
+ ) {
20
+ this.config = {
21
+ token: this.apiKey,
22
+ };
23
+ this.trackPageHit();
24
+ }
25
+ static async init(
26
+ apiKey: string,
27
+ projectId: string
28
+ ): Promise<FormoAnalytics> {
29
+ const config = {
30
+ token: apiKey,
31
+ };
32
+ const instance = new FormoAnalytics(apiKey, projectId);
33
+ instance.config = config;
34
+
35
+ return instance;
36
+ }
37
+
38
+ private identifyUser(userData: any) {
39
+ this.trackEvent('identify', userData);
40
+ }
41
+
42
+ private getSessionId() {
43
+ const existingSessionId = this.getCookieValue(this.sessionIdKey);
44
+
45
+ if (existingSessionId) {
46
+ return existingSessionId;
47
+ }
48
+
49
+ const newSessionId = this.generateSessionId();
50
+ return newSessionId;
51
+ }
52
+
53
+ // Function to set the session cookie
54
+ private setSessionCookie(domain?: string) {
55
+ const sessionId = this.getSessionId();
56
+ let cookieValue = `${this.sessionIdKey}=${sessionId}; Max-Age=1800; path=/; secure`;
57
+ if (domain) {
58
+ cookieValue += `; domain=${domain}`;
59
+ }
60
+ document.cookie = cookieValue;
61
+ }
62
+
63
+ // Function to generate a new session ID
64
+ private generateSessionId(): string {
65
+ return crypto.randomUUID();
66
+ }
67
+
68
+ // Function to get a cookie value by name
69
+ private getCookieValue(name: string): string | undefined {
70
+ const cookies = document.cookie.split(';').reduce((acc, cookie) => {
71
+ const [key, value] = cookie.split('=');
72
+ acc[key.trim()] = value;
73
+ return acc;
74
+ }, {} as Record<string, string>);
75
+ return cookies[name];
76
+ }
77
+
78
+ // Function to send tracking data
79
+ private async trackEvent(action: string, payload: any) {
80
+ const maxRetries = 3;
81
+ let attempt = 0;
82
+
83
+ this.setSessionCookie(this.config.domain);
84
+ const apiUrl = this.buildApiUrl();
85
+
86
+ const requestData = {
87
+ project_id: this.projectId,
88
+ address: '', // TODO: get cached / session wallet address
89
+ session_id: this.getSessionId(),
90
+ timestamp: new Date().toISOString(),
91
+ action: action,
92
+ version: '1',
93
+ payload: isNotEmpty(payload) ? this.maskSensitiveData(payload) : payload,
94
+ };
95
+
96
+ console.log('Request data:', JSON.stringify(requestData));
97
+
98
+ const sendRequest = async (): Promise<void> => {
99
+ try {
100
+ const response = await axios.post(apiUrl, JSON.stringify(requestData), {
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ },
104
+ });
105
+
106
+ if (response.status >= 200 && response.status < 300) {
107
+ console.log('Event sent successfully:', action);
108
+ } else {
109
+ throw new Error(`Failed with status: ${response.status}`);
110
+ }
111
+ } catch (error) {
112
+ attempt++;
113
+ if (attempt <= maxRetries) {
114
+ const retryDelay = Math.pow(2, attempt) * 1000;
115
+ console.error(
116
+ `Attempt ${attempt}: Retrying event "${action}" in ${
117
+ retryDelay / 1000
118
+ } seconds...`
119
+ );
120
+ setTimeout(sendRequest, retryDelay);
121
+ } else {
122
+ console.error(
123
+ `Event "${action}" failed after ${maxRetries} attempts. Error: ${error}`
124
+ );
125
+ }
126
+ }
127
+ };
128
+
129
+ // Start the initial request
130
+ await sendRequest();
131
+ }
132
+
133
+ // Function to mask sensitive data in the payload
134
+ private maskSensitiveData(
135
+ data: string | undefined | null
136
+ ): Record<string, any> | null {
137
+ // Check if data is null or undefined
138
+ if (data === null || data === undefined) {
139
+ console.warn('Data is null or undefined, returning null');
140
+ return null;
141
+ }
142
+
143
+ // Check if data is a string; if so, parse it to an object
144
+ if (typeof data === 'string') {
145
+ let parsedData: Record<string, any>;
146
+ try {
147
+ parsedData = JSON.parse(data);
148
+ } catch (error) {
149
+ console.error('Failed to parse JSON:', error);
150
+ return null; // Return null if parsing fails
151
+ }
152
+
153
+ const sensitiveFields = [
154
+ 'username',
155
+ 'user',
156
+ 'user_id',
157
+ 'password',
158
+ 'email',
159
+ 'phone',
160
+ ];
161
+
162
+ // Create a new object to store masked data
163
+ const maskedData = { ...parsedData };
164
+
165
+ // Mask sensitive fields
166
+ sensitiveFields.forEach((field) => {
167
+ if (field in maskedData) {
168
+ maskedData[field] = '********'; // Replace value with masked string
169
+ }
170
+ });
171
+
172
+ return maskedData; // Return the new object with masked fields
173
+ } else if (typeof data === 'object') {
174
+ // If data is already an object, handle masking directly
175
+ const sensitiveFields = [
176
+ 'username',
177
+ 'user',
178
+ 'user_id',
179
+ 'password',
180
+ 'email',
181
+ 'phone',
182
+ ];
183
+
184
+ const maskedData = { ...(data as Record<string, any>) };
185
+
186
+ // Mask sensitive fields
187
+ sensitiveFields.forEach((field) => {
188
+ if (field in maskedData) {
189
+ maskedData[field] = '********'; // Replace value with masked string
190
+ }
191
+ });
192
+
193
+ return maskedData; // Return the new object with masked fields
194
+ }
195
+
196
+ return data;
197
+ }
198
+
199
+ // Function to track page hits
200
+ private trackPageHit() {
201
+ if (window.__nightmare || window.navigator.webdriver || window.Cypress)
202
+ return;
203
+
204
+ let location: string | undefined;
205
+ let language: string;
206
+ try {
207
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
208
+ location = this.timezoneToCountry[timezone];
209
+ language =
210
+ navigator.languages && navigator.languages.length
211
+ ? navigator.languages[0]
212
+ : navigator.language || 'en';
213
+ } catch (error) {
214
+ console.error('Error resolving timezone or language:', error);
215
+ }
216
+
217
+ setTimeout(() => {
218
+ this.trackEvent('page_hit', {
219
+ 'user-agent': window.navigator.userAgent,
220
+ locale: language,
221
+ location: location,
222
+ referrer: document.referrer,
223
+ pathname: window.location.pathname,
224
+ href: window.location.href,
225
+ });
226
+ }, 300);
227
+ }
228
+
229
+ // Function to build the API URL
230
+ private buildApiUrl(): string {
231
+ const { host, proxy, token, dataSource = 'analytics_events' } = this.config;
232
+ if (token) {
233
+ if (proxy) {
234
+ return `${proxy}/api/tracking`;
235
+ }
236
+ if (host) {
237
+ return `${host.replace(
238
+ /\/+$/,
239
+ ''
240
+ )}/v0/events?name=${dataSource}&token=${token}`;
241
+ }
242
+ return `${EVENTS_API}?name=${dataSource}&token=${token}`;
243
+ }
244
+ return 'Error: No token provided';
245
+ }
246
+
247
+ init(apiKey: string, projectId: string): Promise<FormoAnalytics> {
248
+ const instance = new FormoAnalytics(apiKey, projectId);
249
+
250
+ return Promise.resolve(instance);
251
+ }
252
+
253
+ identify(userData: any) {
254
+ this.identifyUser(userData);
255
+ }
256
+
257
+ page() {
258
+ this.trackPageHit();
259
+ }
260
+
261
+ track(eventName: string, eventData: any) {
262
+ this.trackEvent(eventName, eventData);
263
+ }
264
+ }
@@ -0,0 +1,54 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ useRef,
7
+ } from 'react';
8
+ import { FormoAnalytics } from './FormoAnalytics';
9
+ import { FormoAnalyticsProviderProps } from './types';
10
+
11
+ export const FormoAnalyticsContext = createContext<FormoAnalytics | undefined>(
12
+ undefined
13
+ );
14
+
15
+ export const FormoAnalyticsProvider = ({
16
+ apiKey,
17
+ projectId,
18
+ disabled,
19
+ children,
20
+ }: FormoAnalyticsProviderProps) => {
21
+ const [sdk, setSdk] = useState<FormoAnalytics | undefined>();
22
+ const initializedStartedRef = useRef(false);
23
+
24
+ useEffect(() => {
25
+ if (!apiKey) {
26
+ throw new Error('FormoAnalyticsProvider: No API key provided');
27
+ }
28
+
29
+ if (disabled) return;
30
+
31
+ if (initializedStartedRef.current) return;
32
+ initializedStartedRef.current = true;
33
+
34
+ FormoAnalytics.init(apiKey, projectId).then((sdkInstance) => setSdk(sdkInstance));
35
+ }, [apiKey, disabled]);
36
+
37
+ return (
38
+ <FormoAnalyticsContext.Provider value={sdk}>
39
+ {children}
40
+ </FormoAnalyticsContext.Provider>
41
+ );
42
+ };
43
+
44
+ export const useFormoAnalytics = () => {
45
+ const context = useContext(FormoAnalyticsContext);
46
+
47
+ if (!context) {
48
+ throw new Error(
49
+ 'useFormoAnalytics must be used within a FormoAnalyticsProvider'
50
+ );
51
+ }
52
+
53
+ return context;
54
+ };