@jitsu/js 1.0.0-canary-20230211030946 → 1.0.0-canary-20230219230011

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.
@@ -0,0 +1,103 @@
1
+ import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
2
+ import { isInBrowser, randomId } from "./analytics-plugin";
3
+
4
+ export type InternalPlugin<T> = {
5
+ id: string;
6
+ handle(config: T, payload: AnalyticsClientEvent): Promise<void>;
7
+ };
8
+
9
+ export type CommonDestinationCredentials = {
10
+ hosts?: string[];
11
+ events?: string[];
12
+ };
13
+
14
+ export type TagDestinationCredentials = {
15
+ code: string;
16
+ } & CommonDestinationCredentials;
17
+
18
+ function satisfyFilter(filter: string, subject: string | undefined): boolean {
19
+ return filter === "*" || filter.toLowerCase().trim() === (subject || "").trim().toLowerCase();
20
+ }
21
+
22
+ function applyFilters(event: AnalyticsClientEvent, creds: CommonDestinationCredentials): boolean {
23
+ const { hosts = ["*"], events = ["*"] } = creds;
24
+ return (
25
+ !!hosts.find(hostFilter => satisfyFilter(hostFilter, event.context?.host)) &&
26
+ !!events.find(eventFilter => satisfyFilter(eventFilter, event.type))
27
+ );
28
+ }
29
+
30
+ const tagPlugin: InternalPlugin<TagDestinationCredentials> = {
31
+ id: "tag",
32
+ async handle(config, payload: AnalyticsClientEvent) {
33
+ if (!applyFilters(payload, config)) {
34
+ return;
35
+ }
36
+ insertTags(config.code, payload);
37
+ },
38
+ };
39
+
40
+ function insertTags(code, event: AnalyticsClientEvent, opts: { debug?: boolean } = {}) {
41
+ let tag;
42
+ try {
43
+ tag = JSON.parse(code);
44
+ } catch (e) {
45
+ tag = { code, lang: "javascript" };
46
+ }
47
+ const debug = opts.debug || false;
48
+ if (isInBrowser()) {
49
+ if (tag.lang === "javascript") {
50
+ execJs(tag.code, event);
51
+ } else {
52
+ const codeHolder = document.createElement("span");
53
+ codeHolder.innerHTML = replaceMacro(tag.code, event);
54
+ document.body.insertAdjacentElement("beforeend", codeHolder);
55
+ const scripts = codeHolder.querySelectorAll("script");
56
+ scripts.forEach(script => {
57
+ const scriptClone = document.createElement("script");
58
+ scriptClone.type = scriptClone.type || "text/javascript";
59
+ if (script.hasAttribute("src")) {
60
+ scriptClone.src = script.src;
61
+ }
62
+ scriptClone.text = script.text;
63
+ if (debug) {
64
+ console.log(
65
+ `[JITSU] Executing script${script.hasAttribute("src") ? ` ${script.src}` : ""}`,
66
+ scriptClone.text
67
+ );
68
+ }
69
+ document.head.appendChild(scriptClone);
70
+ document.head.removeChild(scriptClone);
71
+ });
72
+ }
73
+ } else {
74
+ if (debug) {
75
+ console.log(`[JITSU] insertTags(): cannot insert tags in non-browser environment`);
76
+ }
77
+ }
78
+ }
79
+
80
+ function execJs(code: string, event: any) {
81
+ const varName = `jitsu_event_${randomId()}`;
82
+ window[varName] = event;
83
+ const iif = `(function(){
84
+ const event = ${varName};
85
+ ${code}
86
+ })()`;
87
+ try {
88
+ eval(iif);
89
+ } catch (e) {
90
+ console.error(`[JITSU] Error executing JS code: ${e.message}. Code: `, iif);
91
+ } finally {
92
+ delete window[varName];
93
+ }
94
+ return iif;
95
+ }
96
+
97
+ function replaceMacro(code, event) {
98
+ return code.replace(/{{\s*event\s*}}/g, JSON.stringify(event));
99
+ }
100
+
101
+ export const internalDestinationPlugins: Record<string, InternalPlugin<any>> = {
102
+ [tagPlugin.id]: tagPlugin,
103
+ };
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import Analytics from "analytics";
2
- import { JitsuOptions, AnalyticsInterface } from "./jitsu";
3
- import jitsuAnalyticsPlugin, { emptyRuntime, windowRuntime } from "./analytics-plugin";
2
+ import { AnalyticsInterface, JitsuOptions, RuntimeFacade } from "./jitsu";
3
+ import jitsuAnalyticsPlugin, { emptyRuntime, isInBrowser, windowRuntime } from "./analytics-plugin";
4
+ import { delayMethodExec } from "./method-queue";
5
+ import e from "express";
4
6
 
5
7
  export default function parse(input) {
6
8
  let value = input;
@@ -22,13 +24,16 @@ export default function parse(input) {
22
24
  return value;
23
25
  }
24
26
 
25
- export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
26
- const rt = opts.runtime || (typeof window === "undefined" ? emptyRuntime(opts) : windowRuntime(opts));
27
+ function createUnderlyingAnalyticsInstance(
28
+ opts: JitsuOptions,
29
+ rt: RuntimeFacade,
30
+ plugins: any[] = []
31
+ ): AnalyticsInterface {
27
32
  const analytics = Analytics({
28
33
  app: "test",
29
34
  debug: !!opts.debug,
30
35
  storage: rt.store(),
31
- plugins: [jitsuAnalyticsPlugin(opts)],
36
+ plugins: [jitsuAnalyticsPlugin(opts), ...plugins],
32
37
  } as any);
33
38
  const originalPage = analytics.page;
34
39
  analytics.page = (...args) => {
@@ -41,7 +46,36 @@ export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
41
46
  return originalPage(...args);
42
47
  }
43
48
  };
44
- return analytics;
49
+ return analytics as AnalyticsInterface;
50
+ }
51
+
52
+ export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
53
+ const inBrowser = isInBrowser();
54
+ const rt = opts.runtime || (inBrowser ? windowRuntime(opts) : emptyRuntime(opts));
55
+ return createUnderlyingAnalyticsInstance(opts, rt);
56
+
57
+ // if (inBrowser) {
58
+ // const fetch = opts.fetch || globalThis.fetch;
59
+ // if (!fetch) {
60
+ // throw new Error(
61
+ // "Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope"
62
+ // );
63
+ // }
64
+ // const url = `${opts.host}/api/s/cfg`;
65
+ // const authHeader = {};
66
+ // const debugHeader = opts.debug ? { "X-Enable-Debug": "true" } : {};
67
+ // fetch(url)
68
+ // .then(res => res.json())
69
+ // .then(res => {
70
+ // result.loaded(createUnderlyingAnalyticsInstance(opts, rt, []));
71
+ // })
72
+ // .catch(e => {
73
+ // console.warn(`[JITSU] error getting device-destinations from ${url}`, e);
74
+ // result.loaded(createUnderlyingAnalyticsInstance(opts, rt));
75
+ // });
76
+ // } else {
77
+ // result.loaded(createUnderlyingAnalyticsInstance(opts, rt));
78
+ // }
45
79
  }
46
80
 
47
81
  export * from "./jitsu";
@@ -0,0 +1,70 @@
1
+ export type InterfaceWrapper<T> = {
2
+ get(): WithAsyncMethods<T>;
3
+ loaded(instance: T);
4
+ };
5
+
6
+ type MethodCall<M> = {
7
+ method: keyof M;
8
+ args: any[];
9
+ resolve?: (value: any) => void;
10
+ reject?: (reason: any) => void;
11
+ };
12
+
13
+ type WithAsyncMethods<T> = {
14
+ [K in keyof T]: T[K] extends (...args: infer A) => infer R
15
+ ? (...args: A) => R extends Promise<any> ? R : Promise<R>
16
+ : T[K];
17
+ };
18
+
19
+ /**
20
+ * This function creates a wrapper around an interface that allows to call methods on it, but all methods will go to queue. Once actual instance
21
+ * implementation becomes available, all queued methods will be called on it.
22
+ *
23
+ *
24
+ * @param methods of methods that should be wrapped. Per each method of you should specify if it should be wrapped. You'll need to mark all
25
+ * methods for type safety. If method is not wrapped, it will throw an error when called.
26
+ */
27
+ export function delayMethodExec<T>(methods: Record<keyof T, boolean>): InterfaceWrapper<T> {
28
+ const queue: Array<MethodCall<T>> = [];
29
+
30
+ let instance: Partial<T> = {};
31
+ for (const [_method, enabled] of Object.entries(methods)) {
32
+ const method = _method as keyof T;
33
+ if (enabled) {
34
+ instance[method] = ((...args) => {
35
+ queue.push({ method, args });
36
+ return new Promise((resolve, reject) => {
37
+ queue[queue.length - 1].resolve = resolve;
38
+ queue[queue.length - 1].reject = reject;
39
+ });
40
+ }) as any;
41
+ } else {
42
+ instance[method] = (() => {
43
+ throw new Error(`Method ${_method} is not implemented`);
44
+ }) as any;
45
+ }
46
+ }
47
+
48
+ return {
49
+ get() {
50
+ return instance as WithAsyncMethods<T>;
51
+ },
52
+ loaded(newInstance: T) {
53
+ for (const { method, args, resolve, reject } of queue) {
54
+ try {
55
+ const result = (newInstance[method] as any)(...args);
56
+ if (typeof result.then === "function") {
57
+ result.then(resolve).catch(reject);
58
+ } else {
59
+ resolve(result);
60
+ }
61
+ } catch (e) {
62
+ reject(e);
63
+ }
64
+ }
65
+ for (const method of Object.keys(methods)) {
66
+ instance[method] = newInstance[method];
67
+ }
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,51 @@
1
+ function findScript(src: string): HTMLScriptElement | undefined {
2
+ const scripts = Array.prototype.slice.call(window.document.querySelectorAll("script"));
3
+ return scripts.find(s => s.src === src);
4
+ }
5
+
6
+ export function loadScript(src: string, attributes?: Record<string, string>): Promise<HTMLScriptElement> {
7
+ const found = findScript(src);
8
+
9
+ if (found !== undefined) {
10
+ const status = found?.getAttribute("status");
11
+
12
+ if (status === "loaded") {
13
+ return Promise.resolve(found);
14
+ }
15
+
16
+ if (status === "loading") {
17
+ return new Promise((resolve, reject) => {
18
+ found.addEventListener("load", () => resolve(found));
19
+ found.addEventListener("error", err => reject(err));
20
+ });
21
+ }
22
+ }
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const script = window.document.createElement("script");
26
+
27
+ script.type = "text/javascript";
28
+ script.src = src;
29
+ script.async = true;
30
+
31
+ script.setAttribute("status", "loading");
32
+ for (const [k, v] of Object.entries(attributes ?? {})) {
33
+ script.setAttribute(k, v);
34
+ }
35
+
36
+ script.onload = (): void => {
37
+ script.onerror = script.onload = null;
38
+ script.setAttribute("status", "loaded");
39
+ resolve(script);
40
+ };
41
+
42
+ script.onerror = (): void => {
43
+ script.onerror = script.onload = null;
44
+ script.setAttribute("status", "error");
45
+ reject(new Error(`Failed to load ${src}`));
46
+ };
47
+
48
+ const tag = window.document.getElementsByTagName("script")[0];
49
+ tag.parentElement?.insertBefore(script, tag);
50
+ });
51
+ }