@jitsu/js 0.0.0-alpha.95
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/.pnpm-debug.log +23 -0
- package/.turbo/turbo-build.log +15 -0
- package/.turbo/turbo-clean.log +5 -0
- package/__tests__/node/nodejs.test.ts +84 -0
- package/__tests__/playwright/cases/basic.html +27 -0
- package/__tests__/playwright/cases/segment-reference.html +64 -0
- package/__tests__/playwright/integration.test.ts +262 -0
- package/__tests__/simple-syrup.ts +130 -0
- package/dist/jitsu.cjs.js +433 -0
- package/dist/jitsu.d.ts +48 -0
- package/dist/jitsu.es.js +431 -0
- package/dist/web/p.js.txt +489 -0
- package/jest.config.js +13 -0
- package/package.json +49 -0
- package/playwrite.config.ts +91 -0
- package/rollup.config.js +25 -0
- package/src/analytics-plugin.ts +371 -0
- package/src/browser.ts +78 -0
- package/src/index.ts +46 -0
- package/src/jitsu.d.ts +48 -0
- package/tsconfig.json +22 -0
- package/tsconfig.test.json +15 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { devices, PlaywrightTestConfig } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read environment variables from file.
|
|
5
|
+
* https://github.com/motdotla/dotenv
|
|
6
|
+
*/
|
|
7
|
+
// require('dotenv').config();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
11
|
+
*/
|
|
12
|
+
const config: PlaywrightTestConfig = {
|
|
13
|
+
/* Maximum time one test can run for. */
|
|
14
|
+
timeout: 30 * 1000,
|
|
15
|
+
expect: {
|
|
16
|
+
/**
|
|
17
|
+
* Maximum time expect() should wait for the condition to be met.
|
|
18
|
+
* For example in `await expect(locator).toHaveText();`
|
|
19
|
+
*/
|
|
20
|
+
timeout: 5000,
|
|
21
|
+
},
|
|
22
|
+
/* Run tests in files in parallel */
|
|
23
|
+
fullyParallel: true,
|
|
24
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
25
|
+
forbidOnly: !!process.env.CI,
|
|
26
|
+
/* Retry on CI only */
|
|
27
|
+
retries: process.env.CI ? 2 : 0,
|
|
28
|
+
/* Opt out of parallel tests on CI. */
|
|
29
|
+
workers: process.env.CI ? 1 : undefined,
|
|
30
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
31
|
+
reporter: "html",
|
|
32
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
33
|
+
use: {
|
|
34
|
+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
|
35
|
+
actionTimeout: 0,
|
|
36
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
37
|
+
// baseURL: 'http://localhost:3000',
|
|
38
|
+
|
|
39
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
40
|
+
trace: "on-first-retry",
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/* Configure projects for major browsers */
|
|
44
|
+
projects: [
|
|
45
|
+
{
|
|
46
|
+
name: "chromium",
|
|
47
|
+
use: {
|
|
48
|
+
...devices["Desktop Chrome"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/* Test against mobile viewports. */
|
|
53
|
+
// {
|
|
54
|
+
// name: 'Mobile Chrome',
|
|
55
|
+
// use: {
|
|
56
|
+
// ...devices['Pixel 5'],
|
|
57
|
+
// },
|
|
58
|
+
// },
|
|
59
|
+
// {
|
|
60
|
+
// name: 'Mobile Safari',
|
|
61
|
+
// use: {
|
|
62
|
+
// ...devices['iPhone 12'],
|
|
63
|
+
// },
|
|
64
|
+
// },
|
|
65
|
+
|
|
66
|
+
/* Test against branded browsers. */
|
|
67
|
+
// {
|
|
68
|
+
// name: 'Microsoft Edge',
|
|
69
|
+
// use: {
|
|
70
|
+
// channel: 'msedge',
|
|
71
|
+
// },
|
|
72
|
+
// },
|
|
73
|
+
// {
|
|
74
|
+
// name: 'Google Chrome',
|
|
75
|
+
// use: {
|
|
76
|
+
// channel: 'chrome',
|
|
77
|
+
// },
|
|
78
|
+
// },
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
|
82
|
+
outputDir: "__tests__/results",
|
|
83
|
+
|
|
84
|
+
/* Run your local dev server before starting the tests */
|
|
85
|
+
// webServer: {
|
|
86
|
+
// command: 'npm run start',
|
|
87
|
+
// port: 3000,
|
|
88
|
+
// },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default config;
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const multi = require("@rollup/plugin-multi-entry");
|
|
2
|
+
const resolve = require("@rollup/plugin-node-resolve");
|
|
3
|
+
const commonjs = require("@rollup/plugin-commonjs");
|
|
4
|
+
const rollupJson = require("@rollup/plugin-json");
|
|
5
|
+
const terser = require("@rollup/plugin-terser");
|
|
6
|
+
|
|
7
|
+
module.exports = [
|
|
8
|
+
{
|
|
9
|
+
plugins: [multi(), resolve({ preferBuiltins: false }), commonjs(), rollupJson(),],
|
|
10
|
+
input: "./compiled/src/browser.js",
|
|
11
|
+
output: {
|
|
12
|
+
file: `dist/web/p.js.txt`,
|
|
13
|
+
format: "iife",
|
|
14
|
+
sourcemap: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
plugins: [multi(), resolve({ preferBuiltins: false }), commonjs(), rollupJson()],
|
|
19
|
+
input: "./compiled/src/index.js",
|
|
20
|
+
output: [
|
|
21
|
+
{ file: "dist/jitsu.es.js", format: "es" },
|
|
22
|
+
{ file: "dist/jitsu.cjs.js", format: "cjs" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/* global analytics */
|
|
2
|
+
|
|
3
|
+
import { JitsuOptions, PersistentStorage, RuntimeFacade } from "./jitsu";
|
|
4
|
+
import { AnalyticsClientEvent } from "@jitsu/types/analytics";
|
|
5
|
+
import parse from "./index";
|
|
6
|
+
import { AnalyticsPlugin } from "analytics";
|
|
7
|
+
|
|
8
|
+
const config: JitsuOptions = {
|
|
9
|
+
/* Your segment writeKey */
|
|
10
|
+
writeKey: null,
|
|
11
|
+
/* Disable anonymous MTU */
|
|
12
|
+
host: null,
|
|
13
|
+
debug: false,
|
|
14
|
+
fetch: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const parseQuery = (qs?: string): Record<string, string> => {
|
|
18
|
+
if (!qs) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
let queryString = qs.length > 0 && qs.charAt(0) === "?" ? qs.substring(1) : qs;
|
|
22
|
+
let query: Record<string, string> = {};
|
|
23
|
+
let pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&");
|
|
24
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
25
|
+
let pair = pairs[i].split("=");
|
|
26
|
+
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
|
|
27
|
+
}
|
|
28
|
+
return query;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function utmToKey(key) {
|
|
32
|
+
const name = key.substring("utm_".length);
|
|
33
|
+
return name === "campaign" ? "name" : name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseUtms(query: Record<string, string>) {
|
|
37
|
+
return Object.entries(query)
|
|
38
|
+
.filter(([key]) => key.indexOf("utm_") === 0)
|
|
39
|
+
.reduce(
|
|
40
|
+
(acc, [key, value]) => ({
|
|
41
|
+
...acc,
|
|
42
|
+
[utmToKey(key)]: value,
|
|
43
|
+
}),
|
|
44
|
+
{}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeCall<T>(f: () => T, defaultVal?: T): T | undefined {
|
|
49
|
+
try {
|
|
50
|
+
return f();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return defaultVal;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function restoreTraits(storage: PersistentStorage) {
|
|
57
|
+
const val = storage.getItem("__user_traits");
|
|
58
|
+
if (typeof val === "string") {
|
|
59
|
+
return safeCall(() => JSON.parse(val), {});
|
|
60
|
+
}
|
|
61
|
+
return val;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type StorageFactory = (cookieDomain: string, cookie2key: Record<string, string>) => PersistentStorage;
|
|
65
|
+
|
|
66
|
+
export function getTopLevelDomain(opts: JitsuOptions = {}) {
|
|
67
|
+
if (opts.cookieDomain) {
|
|
68
|
+
return opts.cookieDomain;
|
|
69
|
+
}
|
|
70
|
+
const [domain] = window.location.hostname.split(":");
|
|
71
|
+
const parts = domain.split(".");
|
|
72
|
+
if (parts[parts.length - 1] === "localhost" || parts.length < 2) {
|
|
73
|
+
return parts[parts.length - 1];
|
|
74
|
+
} else {
|
|
75
|
+
return parts[parts.length - 2] + "." + parts[parts.length - 1];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getCookie(name: string) {
|
|
80
|
+
const value = `; ${document.cookie}`;
|
|
81
|
+
const parts = value.split(`; ${name}=`);
|
|
82
|
+
return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function removeCookie(name: string) {
|
|
86
|
+
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setCookie(name: string, val: string, { domain, secure }: { domain: string; secure: boolean }) {
|
|
90
|
+
document.cookie =
|
|
91
|
+
name +
|
|
92
|
+
"=" +
|
|
93
|
+
val +
|
|
94
|
+
";domain=" +
|
|
95
|
+
domain +
|
|
96
|
+
";path=/" +
|
|
97
|
+
";expires=" +
|
|
98
|
+
new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5).toUTCString() +
|
|
99
|
+
";SameSite=" +
|
|
100
|
+
(secure ? "None" : "Lax") +
|
|
101
|
+
(secure ? ";secure" : "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const defaultCookie2Key = {
|
|
105
|
+
__anon_id: "__eventn_id",
|
|
106
|
+
__user_traits: "__eventn_id_usr",
|
|
107
|
+
__user_id: "__eventn_uid",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const cookieStorage: StorageFactory = (cookieDomain, key2cookie) => {
|
|
111
|
+
return {
|
|
112
|
+
setItem(key: string, val: any) {
|
|
113
|
+
const strVal = typeof val === "object" && val !== null ? JSON.stringify(val) : val;
|
|
114
|
+
const cookieName = key2cookie[key] || key;
|
|
115
|
+
setCookie(cookieName, strVal, {
|
|
116
|
+
domain: cookieDomain,
|
|
117
|
+
secure: window.location.protocol === "https:",
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
getItem(key: string) {
|
|
121
|
+
const cookieName = key2cookie[key] || key;
|
|
122
|
+
const result = getCookie(cookieName);
|
|
123
|
+
return parse(result);
|
|
124
|
+
},
|
|
125
|
+
removeItem(key: string) {
|
|
126
|
+
removeCookie(key2cookie[key] || key);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function windowRuntime(opts: JitsuOptions): RuntimeFacade {
|
|
132
|
+
return {
|
|
133
|
+
documentEncoding(): string | undefined {
|
|
134
|
+
return window.document.characterSet;
|
|
135
|
+
},
|
|
136
|
+
timezoneOffset(): number | undefined {
|
|
137
|
+
return new Date().getTimezoneOffset();
|
|
138
|
+
},
|
|
139
|
+
store(): PersistentStorage {
|
|
140
|
+
return cookieStorage(opts.cookieDomain || getTopLevelDomain(), defaultCookie2Key);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
language(): string {
|
|
144
|
+
return window.navigator.language;
|
|
145
|
+
},
|
|
146
|
+
pageTitle(): string {
|
|
147
|
+
return window.document.title;
|
|
148
|
+
},
|
|
149
|
+
pageUrl(): string {
|
|
150
|
+
return window.location.href;
|
|
151
|
+
},
|
|
152
|
+
referrer(): string {
|
|
153
|
+
return window.document.referrer;
|
|
154
|
+
},
|
|
155
|
+
screen(): { width: number; height: number; innerWidth: number; innerHeight: number; density: number } {
|
|
156
|
+
return {
|
|
157
|
+
width: window.screen.width,
|
|
158
|
+
height: window.screen.height,
|
|
159
|
+
innerWidth: window.innerWidth,
|
|
160
|
+
innerHeight: window.innerHeight,
|
|
161
|
+
density: window.devicePixelRatio,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
userAgent(): string {
|
|
165
|
+
return window.navigator.userAgent;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const emptyRuntime = (config: JitsuOptions): RuntimeFacade => ({
|
|
171
|
+
documentEncoding(): string | undefined {
|
|
172
|
+
return undefined;
|
|
173
|
+
},
|
|
174
|
+
timezoneOffset(): number | undefined {
|
|
175
|
+
return undefined;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
store(): PersistentStorage {
|
|
179
|
+
const storage = {};
|
|
180
|
+
return {
|
|
181
|
+
setItem(key: string, val: any) {
|
|
182
|
+
if (config.debug) {
|
|
183
|
+
console.log(`[JITSU EMPTY RUNTIME] Set storage item ${key}=${JSON.stringify(val)}`);
|
|
184
|
+
}
|
|
185
|
+
storage[key] = val;
|
|
186
|
+
},
|
|
187
|
+
getItem(key: string) {
|
|
188
|
+
const val = storage[key];
|
|
189
|
+
if (config.debug) {
|
|
190
|
+
console.log(`[JITSU EMPTY RUNTIME] Get storage item ${key}=${JSON.stringify(val)}`);
|
|
191
|
+
}
|
|
192
|
+
return val;
|
|
193
|
+
},
|
|
194
|
+
removeItem(key: string) {
|
|
195
|
+
if (config.debug) {
|
|
196
|
+
console.log(`[JITSU EMPTY RUNTIME] Get storage item ${key}=${storage[key]}`);
|
|
197
|
+
}
|
|
198
|
+
delete storage[key];
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
language() {
|
|
203
|
+
return undefined;
|
|
204
|
+
},
|
|
205
|
+
pageTitle() {
|
|
206
|
+
return undefined;
|
|
207
|
+
},
|
|
208
|
+
pageUrl() {
|
|
209
|
+
return undefined;
|
|
210
|
+
},
|
|
211
|
+
referrer() {
|
|
212
|
+
return undefined;
|
|
213
|
+
},
|
|
214
|
+
screen() {
|
|
215
|
+
return undefined;
|
|
216
|
+
},
|
|
217
|
+
userAgent() {
|
|
218
|
+
return undefined;
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
function deepMerge(target: any, source: any) {
|
|
223
|
+
if (typeof source !== "object" || source === null) {
|
|
224
|
+
return source;
|
|
225
|
+
}
|
|
226
|
+
if (typeof target !== "object" || target === null) {
|
|
227
|
+
return source;
|
|
228
|
+
}
|
|
229
|
+
return Object.entries(source).reduce((acc, [key, value]) => {
|
|
230
|
+
acc[key] = deepMerge(target[key], value);
|
|
231
|
+
return acc;
|
|
232
|
+
}, target);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentStorage): AnalyticsClientEvent {
|
|
236
|
+
const runtime: RuntimeFacade =
|
|
237
|
+
config.runtime || (typeof window === "undefined" ? emptyRuntime(config) : windowRuntime(config));
|
|
238
|
+
const url = runtime.pageUrl();
|
|
239
|
+
const parsedUrl = safeCall(() => new URL(url), undefined);
|
|
240
|
+
const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
|
|
241
|
+
const properties = payload.properties || {};
|
|
242
|
+
const customContext = payload.properties?.context || {};
|
|
243
|
+
delete payload.properties?.context;
|
|
244
|
+
const referrer = runtime.referrer();
|
|
245
|
+
const context = {
|
|
246
|
+
library: {
|
|
247
|
+
name: "jitsu-js",
|
|
248
|
+
version: "1.0.0",
|
|
249
|
+
},
|
|
250
|
+
userAgent: runtime.userAgent(),
|
|
251
|
+
locale: runtime.language(),
|
|
252
|
+
screen: runtime.screen(),
|
|
253
|
+
traits: payload.type != "identify" ? { ...(restoreTraits(storage) || {}) } : undefined,
|
|
254
|
+
page: {
|
|
255
|
+
path: properties.path || (parsedUrl && parsedUrl.pathname),
|
|
256
|
+
referrer: referrer,
|
|
257
|
+
referring_domain: safeCall(() => referrer && new URL(referrer).hostname),
|
|
258
|
+
host: parsedUrl && parsedUrl.host,
|
|
259
|
+
search: properties.search || (parsedUrl && parsedUrl.search),
|
|
260
|
+
title: properties.title || runtime.pageTitle(),
|
|
261
|
+
url: properties.url || url,
|
|
262
|
+
enconding: properties.enconding || runtime.documentEncoding(),
|
|
263
|
+
},
|
|
264
|
+
campaign: parseUtms(query),
|
|
265
|
+
};
|
|
266
|
+
const withContext = {
|
|
267
|
+
...payload,
|
|
268
|
+
timestamp: new Date().toISOString(),
|
|
269
|
+
sentAt: new Date().toISOString(),
|
|
270
|
+
context: deepMerge(context, customContext),
|
|
271
|
+
};
|
|
272
|
+
delete withContext.meta;
|
|
273
|
+
delete withContext.options;
|
|
274
|
+
return withContext;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function send(method, payload, jitsuConfig: Required<JitsuOptions>, store: PersistentStorage): Promise<void> {
|
|
278
|
+
const url = `${jitsuConfig.host}/s/${method}`;
|
|
279
|
+
const fetch = jitsuConfig.fetch || globalThis.fetch;
|
|
280
|
+
if (!fetch) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const authHeader = jitsuConfig.writeKey ? { Authorization: `Bearer ${jitsuConfig.writeKey}` } : {};
|
|
286
|
+
const debugHeader = jitsuConfig.debug ? { "X-Enable-Debug": "true" } : {};
|
|
287
|
+
|
|
288
|
+
if (jitsuConfig.debug) {
|
|
289
|
+
console.log(`Sending jitsu event to ${url}: `, JSON.stringify(payload, null, 2));
|
|
290
|
+
}
|
|
291
|
+
const adjustedPayload = adjustPayload(payload, jitsuConfig, store);
|
|
292
|
+
return fetch(url, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: {
|
|
295
|
+
"Content-Type": "application/json",
|
|
296
|
+
|
|
297
|
+
...authHeader,
|
|
298
|
+
...debugHeader,
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify(adjustedPayload),
|
|
301
|
+
})
|
|
302
|
+
.then(res => {
|
|
303
|
+
if (jitsuConfig.debug) {
|
|
304
|
+
console.debug(
|
|
305
|
+
`Jitsu ${url} replied ${res.status}. Original payload: `,
|
|
306
|
+
JSON.stringify(adjustedPayload, null, 2)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
.catch(err => {
|
|
311
|
+
if (jitsuConfig.debug) {
|
|
312
|
+
console.error(`Jitsu ${url} failed: `, err);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin => {
|
|
318
|
+
const storageCache: any = {};
|
|
319
|
+
// AnalyticsInstance's storage is async somewhere inside. So if we make 'page' call right after 'identify' call
|
|
320
|
+
// 'page' call will load traits from storage before 'identify' call had a change to save them.
|
|
321
|
+
// to avoid that we use in-memory cache for storage
|
|
322
|
+
const cachingStorageWrapper = (persistentStorage: PersistentStorage): PersistentStorage => ({
|
|
323
|
+
setItem(key: string, val: any) {
|
|
324
|
+
storageCache[key] = val;
|
|
325
|
+
persistentStorage.setItem(key, val);
|
|
326
|
+
},
|
|
327
|
+
getItem(key: string) {
|
|
328
|
+
return storageCache[key] || persistentStorage.getItem(key);
|
|
329
|
+
},
|
|
330
|
+
removeItem(key: string) {
|
|
331
|
+
delete storageCache[key];
|
|
332
|
+
persistentStorage.removeItem(key);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
name: "jitsu",
|
|
337
|
+
config: {
|
|
338
|
+
...config,
|
|
339
|
+
...pluginConfig,
|
|
340
|
+
},
|
|
341
|
+
initialize: args => {
|
|
342
|
+
const { config, instance } = args;
|
|
343
|
+
if (config.debug) {
|
|
344
|
+
console.debug("Initializing Jitsu plugin with config: ", JSON.stringify(config));
|
|
345
|
+
}
|
|
346
|
+
if (!config.host) {
|
|
347
|
+
throw new Error("Please specify host variable in jitsu plugin initialization");
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
page: args => {
|
|
351
|
+
const { payload, config, instance } = args;
|
|
352
|
+
return send("page", payload, config, cachingStorageWrapper(instance.storage));
|
|
353
|
+
},
|
|
354
|
+
track: args => {
|
|
355
|
+
const { payload, config, instance } = args;
|
|
356
|
+
return send("track", payload, config, cachingStorageWrapper(instance.storage));
|
|
357
|
+
},
|
|
358
|
+
identify: args => {
|
|
359
|
+
const { payload, config, instance } = args;
|
|
360
|
+
// Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
|
|
361
|
+
storageCache["__user_traits"] = payload.traits;
|
|
362
|
+
return send("identify", payload, config, cachingStorageWrapper(instance.storage));
|
|
363
|
+
},
|
|
364
|
+
reset: args => {
|
|
365
|
+
//clear storage cache
|
|
366
|
+
Object.keys(storageCache).forEach(key => delete storageCache[key]);
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export default jitsuAnalyticsPlugin;
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { JitsuOptions } from "./jitsu";
|
|
2
|
+
import { jitsuAnalytics } from "./index";
|
|
3
|
+
|
|
4
|
+
export type JitsuBrowserOptions = {
|
|
5
|
+
userId?: string;
|
|
6
|
+
onload?: string;
|
|
7
|
+
initOnly?: boolean;
|
|
8
|
+
} & JitsuOptions;
|
|
9
|
+
|
|
10
|
+
function snakeToCamel(s: string) {
|
|
11
|
+
return s.replace(/([-_][a-z])/gi, $1 => {
|
|
12
|
+
return $1.toUpperCase().replace("-", "").replace("_", "");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Parser = (arg: string) => any;
|
|
17
|
+
const booleanParser = (arg: string) => arg === "true" || arg === "1" || arg === "yes";
|
|
18
|
+
|
|
19
|
+
const parsers: Partial<Record<keyof JitsuBrowserOptions, Parser>> = {
|
|
20
|
+
debug: booleanParser,
|
|
21
|
+
initOnly: booleanParser,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getParser(name: keyof JitsuBrowserOptions): Parser {
|
|
25
|
+
return parsers[name] || (x => x);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getScriptAttributes(scriptElement: HTMLScriptElement) {
|
|
29
|
+
return scriptElement
|
|
30
|
+
.getAttributeNames()
|
|
31
|
+
.filter(name => name.indexOf("data-") === 0)
|
|
32
|
+
.map(name => name.substring("data-".length))
|
|
33
|
+
.reduce(
|
|
34
|
+
(res, name) => ({
|
|
35
|
+
...res,
|
|
36
|
+
[snakeToCamel(name)]: getParser(snakeToCamel(name) as any)(scriptElement.getAttribute(`data-${name}`)),
|
|
37
|
+
}),
|
|
38
|
+
{}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
(function () {
|
|
43
|
+
if (window["jitsu"]) {
|
|
44
|
+
console.log("Jitsu is already initialized");
|
|
45
|
+
}
|
|
46
|
+
function readJitsuOptions(): JitsuBrowserOptions {
|
|
47
|
+
const scriptElement = window.document.currentScript as HTMLScriptElement;
|
|
48
|
+
if (!scriptElement) {
|
|
49
|
+
throw new Error(`Can't find script element`);
|
|
50
|
+
}
|
|
51
|
+
const host = new URL(scriptElement.src).origin;
|
|
52
|
+
|
|
53
|
+
return { ...((window as any)?.jitsuConfig || {}), ...getScriptAttributes(scriptElement), host };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const options = readJitsuOptions();
|
|
57
|
+
if (options.debug) {
|
|
58
|
+
console.log(`Jitsu options: `, JSON.stringify(options));
|
|
59
|
+
}
|
|
60
|
+
const jitsu = jitsuAnalytics(options);
|
|
61
|
+
|
|
62
|
+
if (options.onload) {
|
|
63
|
+
const onloadFunction = window[options.onload] as any;
|
|
64
|
+
if (!onloadFunction) {
|
|
65
|
+
console.warn(`onload function ${options.onload} is not found in window`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof onloadFunction === "function") {
|
|
68
|
+
onloadFunction(jitsu);
|
|
69
|
+
} else {
|
|
70
|
+
console.warn(`onload function ${options.onload} is not callable: ${typeof onloadFunction}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
window["jitsu"] = jitsu;
|
|
74
|
+
|
|
75
|
+
if (!options.initOnly) {
|
|
76
|
+
jitsu.page();
|
|
77
|
+
}
|
|
78
|
+
})();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AnalyticsInterface } from "@jitsu/types/analytics.d";
|
|
2
|
+
import Analytics from "analytics";
|
|
3
|
+
import { JitsuOptions, PersistentStorage } from "./jitsu";
|
|
4
|
+
import jitsuAnalyticsPlugin, { emptyRuntime, windowRuntime } from "./analytics-plugin";
|
|
5
|
+
|
|
6
|
+
export default function parse(input) {
|
|
7
|
+
let value = input;
|
|
8
|
+
if (input?.indexOf("%7B%22") === 0) {
|
|
9
|
+
value = decodeURIComponent(input);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
value = JSON.parse(input);
|
|
13
|
+
if (value === "true") return true;
|
|
14
|
+
if (value === "false") return false;
|
|
15
|
+
if (typeof value === "object") return value;
|
|
16
|
+
if (parseFloat(value) === value) {
|
|
17
|
+
value = parseFloat(value);
|
|
18
|
+
}
|
|
19
|
+
} catch (e) {}
|
|
20
|
+
if (value === null || value === "") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
|
|
27
|
+
const rt = opts.runtime || (typeof window === "undefined" ? emptyRuntime(opts) : windowRuntime(opts));
|
|
28
|
+
const analytics = Analytics({
|
|
29
|
+
app: "test",
|
|
30
|
+
debug: !!opts.debug,
|
|
31
|
+
storage: rt.store(),
|
|
32
|
+
plugins: [jitsuAnalyticsPlugin(opts)],
|
|
33
|
+
} as any);
|
|
34
|
+
const originalPage = analytics.page;
|
|
35
|
+
analytics.page = (...args) => {
|
|
36
|
+
if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") {
|
|
37
|
+
return originalPage({
|
|
38
|
+
name: args[0],
|
|
39
|
+
...args[1],
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
return originalPage(...args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return analytics;
|
|
46
|
+
}
|
package/src/jitsu.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AnalyticsInterface } from "@jitsu/types/analytics";
|
|
2
|
+
import type { AnalyticsPlugin } from "analytics";
|
|
3
|
+
|
|
4
|
+
export type JitsuOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* API Key. Optional. If not set, Jitsu will send event to the server without auth, and server
|
|
7
|
+
* will link the call to configured source by domain name
|
|
8
|
+
*/
|
|
9
|
+
writeKey?: string;
|
|
10
|
+
/**
|
|
11
|
+
* API Host. Default value: same host as script origin
|
|
12
|
+
*/
|
|
13
|
+
host?: string;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
cookieDomain?: string;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
runtime?: RuntimeFacade;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PersistentStorage = {
|
|
21
|
+
getItem: (key: string, options?: any) => any;
|
|
22
|
+
setItem: (key: string, value: any, options?: any) => void;
|
|
23
|
+
removeItem: (key: string, options?: any) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RuntimeFacade = {
|
|
27
|
+
store(): PersistentStorage;
|
|
28
|
+
userAgent(): string | undefined;
|
|
29
|
+
language(): string | undefined;
|
|
30
|
+
pageUrl(): string | undefined;
|
|
31
|
+
documentEncoding(): string | undefined;
|
|
32
|
+
timezoneOffset(): number | undefined;
|
|
33
|
+
screen():
|
|
34
|
+
| {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
innerWidth: number;
|
|
38
|
+
innerHeight: number;
|
|
39
|
+
density: number;
|
|
40
|
+
}
|
|
41
|
+
| undefined;
|
|
42
|
+
referrer(): string | undefined;
|
|
43
|
+
pageTitle(): string | undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export declare function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface;
|
|
47
|
+
|
|
48
|
+
declare const jitsuAnalyticsPlugin: AnalyticsPlugin;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": ".",
|
|
4
|
+
"outDir": "./compiled",
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"moduleResolution": "Node",
|
|
8
|
+
"target": "ES2015",
|
|
9
|
+
"lib": ["es2017", "dom"],
|
|
10
|
+
//this makes typescript igore @types/node during compilation
|
|
11
|
+
"types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["./src", "./__tests__"],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"__tests__",
|
|
16
|
+
"node_modules",
|
|
17
|
+
"dist",
|
|
18
|
+
"test_projects",
|
|
19
|
+
"test",
|
|
20
|
+
"templates"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": ".",
|
|
4
|
+
"outDir": "./compiled",
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"moduleResolution": "Node",
|
|
8
|
+
"target": "ES2015",
|
|
9
|
+
"lib": ["es2017", "dom"]
|
|
10
|
+
//this makes typescript igore @types/node during compilation
|
|
11
|
+
// "types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["./src", "./__tests__"],
|
|
14
|
+
"exclude": ["__tests__", "node_modules", "dist", "test_projects", "test", "templates"]
|
|
15
|
+
}
|