@lalternative/nakoda-sdk-browser 0.2.0 → 0.3.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/dist/index.js +23 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
// src/tracker.ts
|
|
2
2
|
var DEFAULT_ENDPOINT = "https://nakoda.club";
|
|
3
3
|
var SOURCE_KEY = "nakoda.source";
|
|
4
|
+
var IGNORE_KEY = "nakoda.ignore";
|
|
5
|
+
var IGNORE_PARAM = "nakoda";
|
|
4
6
|
var LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
5
7
|
function isLocal(hostname) {
|
|
6
8
|
return LOCAL_HOSTS.includes(hostname) || hostname.endsWith(".localhost");
|
|
7
9
|
}
|
|
10
|
+
function isIgnored(href, device2) {
|
|
11
|
+
try {
|
|
12
|
+
const asked = new URL(href).searchParams.get(IGNORE_PARAM);
|
|
13
|
+
if (asked === "ignore") device2?.setItem(IGNORE_KEY, "1");
|
|
14
|
+
if (asked === "count") device2?.removeItem(IGNORE_KEY);
|
|
15
|
+
if (asked === "ignore") return true;
|
|
16
|
+
return device2?.getItem(IGNORE_KEY) === "1";
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
8
21
|
function readSource(storage2) {
|
|
9
22
|
try {
|
|
10
23
|
const raw = storage2?.getItem(SOURCE_KEY);
|
|
@@ -25,7 +38,7 @@ function createTracker(options = {}, env) {
|
|
|
25
38
|
return {
|
|
26
39
|
pageview() {
|
|
27
40
|
const href = env.href();
|
|
28
|
-
if (href === previous || isLocal(env.hostname())) return;
|
|
41
|
+
if (href === previous || isLocal(env.hostname()) || isIgnored(href, env.device)) return;
|
|
29
42
|
const referrer = previous ?? env.referrer;
|
|
30
43
|
previous = href;
|
|
31
44
|
env.send(endpoint, JSON.stringify({ u: href, r: referrer }));
|
|
@@ -48,6 +61,13 @@ function storage() {
|
|
|
48
61
|
return void 0;
|
|
49
62
|
}
|
|
50
63
|
}
|
|
64
|
+
function device() {
|
|
65
|
+
try {
|
|
66
|
+
return window.localStorage;
|
|
67
|
+
} catch {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
51
71
|
function init(options) {
|
|
52
72
|
if (typeof window === "undefined") return () => void 0;
|
|
53
73
|
const tracker = createTracker(options, {
|
|
@@ -55,7 +75,8 @@ function init(options) {
|
|
|
55
75
|
hostname: () => location.hostname,
|
|
56
76
|
referrer: document.referrer,
|
|
57
77
|
send,
|
|
58
|
-
storage: storage()
|
|
78
|
+
storage: storage(),
|
|
79
|
+
device: device()
|
|
59
80
|
});
|
|
60
81
|
tracker.pageview();
|
|
61
82
|
const pushState = history.pushState;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tracker.ts","../src/index.ts"],"sourcesContent":["export interface NakodaOptions {\n /** nakoda's origin; https://nakoda.club by default. */\n endpoint?: string;\n /** @deprecated nakoda knows the app by the page's domain; ignored. */\n site?: string;\n}\n\n/** Where the visit started, kept for the visit so a sign-up can say it. */\nexport interface VisitSource {\n landing: string;\n referrer: string;\n}\n\nexport interface TrackerEnv {\n href: () => string;\n hostname: () => string;\n referrer: string;\n send: (url: string, body: string) => void;\n storage?: Pick<Storage, 'getItem' | 'setItem'>;\n}\n\nconst DEFAULT_ENDPOINT = 'https://nakoda.club';\nconst SOURCE_KEY = 'nakoda.source';\nconst LOCAL_HOSTS = ['localhost', '127.0.0.1', '[::1]'];\n\nexport function isLocal(hostname: string): boolean {\n return LOCAL_HOSTS.includes(hostname) || hostname.endsWith('.localhost');\n}\n\nexport function readSource(storage: TrackerEnv['storage']): VisitSource | null {\n try {\n const raw = storage?.getItem(SOURCE_KEY);\n return raw ? (JSON.parse(raw) as VisitSource) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * A tracker sends one page view per distinct URL. The first carries the\n * page's referrer; the next ones, the previous URL of the same site, which\n * nakoda reads as the same visit going on.\n */\nexport function createTracker(options: NakodaOptions = {}, env: TrackerEnv) {\n const endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\\/$/, '') + '/v1/hit';\n let previous: string | null = null;\n\n if (!readSource(env.storage)) {\n try {\n env.storage?.setItem(SOURCE_KEY, JSON.stringify({ landing: env.href(), referrer: env.referrer }));\n } catch {\n // A browser that refuses storage still counts its page views.\n }\n }\n\n return {\n pageview(): void {\n const href = env.href();\n if (href === previous || isLocal(env.hostname())) return;\n const referrer = previous ?? env.referrer;\n previous = href;\n env.send(endpoint, JSON.stringify({ u: href, r: referrer }));\n },\n };\n}\n","import { createTracker, readSource } from './tracker.ts';\nimport type { NakodaOptions, VisitSource } from './tracker.ts';\n\nexport type { NakodaOptions, VisitSource };\n\nfunction send(url: string, body: string): void {\n // text/plain keeps the request simple: no CORS preflight, and sendBeacon\n // outlives the page being left.\n const blob = new Blob([body], { type: 'text/plain' });\n if (navigator.sendBeacon?.(url, blob)) return;\n void fetch(url, { method: 'POST', body, keepalive: true, headers: { 'content-type': 'text/plain' } }).catch(\n () => undefined,\n );\n}\n\nfunction storage(): Storage | undefined {\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Counts the page's views with nakoda, the History API's navigations\n * included, and keeps where the visit came from. Returns what stops it.\n */\nexport function init(options: NakodaOptions): () => void {\n if (typeof window === 'undefined') return () => undefined;\n const tracker = createTracker(options, {\n href: () => location.href,\n hostname: () => location.hostname,\n referrer: document.referrer,\n send,\n storage: storage(),\n });\n tracker.pageview();\n\n const pushState = history.pushState;\n const replaceState = history.replaceState;\n history.pushState = function (this: History, ...args: Parameters<History['pushState']>) {\n pushState.apply(this, args);\n tracker.pageview();\n };\n history.replaceState = function (this: History, ...args: Parameters<History['replaceState']>) {\n replaceState.apply(this, args);\n tracker.pageview();\n };\n const onPop = () => tracker.pageview();\n window.addEventListener('popstate', onPop);\n\n return () => {\n history.pushState = pushState;\n history.replaceState = replaceState;\n window.removeEventListener('popstate', onPop);\n };\n}\n\n/** Where the current visit started: the landing URL and its referrer. */\nexport function getSource(): VisitSource | null {\n if (typeof window === 'undefined') return null;\n return readSource(storage());\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/tracker.ts","../src/index.ts"],"sourcesContent":["export interface NakodaOptions {\n /** nakoda's origin; https://nakoda.club by default. */\n endpoint?: string;\n /** @deprecated nakoda knows the app by the page's domain; ignored. */\n site?: string;\n}\n\n/** Where the visit started, kept for the visit so a sign-up can say it. */\nexport interface VisitSource {\n landing: string;\n referrer: string;\n}\n\nexport interface TrackerEnv {\n href: () => string;\n hostname: () => string;\n referrer: string;\n send: (url: string, body: string) => void;\n storage?: Pick<Storage, 'getItem' | 'setItem'>;\n device?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n}\n\nconst DEFAULT_ENDPOINT = 'https://nakoda.club';\nconst SOURCE_KEY = 'nakoda.source';\nconst IGNORE_KEY = 'nakoda.ignore';\nconst IGNORE_PARAM = 'nakoda';\nconst LOCAL_HOSTS = ['localhost', '127.0.0.1', '[::1]'];\n\nexport function isLocal(hostname: string): boolean {\n return LOCAL_HOSTS.includes(hostname) || hostname.endsWith('.localhost');\n}\n\n/**\n * Whether this browser asked not to be counted. Opening a page with\n * ?nakoda=ignore records the choice on the device, ?nakoda=count undoes it.\n */\nexport function isIgnored(href: string, device: TrackerEnv['device']): boolean {\n try {\n const asked = new URL(href).searchParams.get(IGNORE_PARAM);\n if (asked === 'ignore') device?.setItem(IGNORE_KEY, '1');\n if (asked === 'count') device?.removeItem(IGNORE_KEY);\n if (asked === 'ignore') return true;\n return device?.getItem(IGNORE_KEY) === '1';\n } catch {\n return false;\n }\n}\n\nexport function readSource(storage: TrackerEnv['storage']): VisitSource | null {\n try {\n const raw = storage?.getItem(SOURCE_KEY);\n return raw ? (JSON.parse(raw) as VisitSource) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * A tracker sends one page view per distinct URL. The first carries the\n * page's referrer; the next ones, the previous URL of the same site, which\n * nakoda reads as the same visit going on.\n */\nexport function createTracker(options: NakodaOptions = {}, env: TrackerEnv) {\n const endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\\/$/, '') + '/v1/hit';\n let previous: string | null = null;\n\n if (!readSource(env.storage)) {\n try {\n env.storage?.setItem(SOURCE_KEY, JSON.stringify({ landing: env.href(), referrer: env.referrer }));\n } catch {\n // A browser that refuses storage still counts its page views.\n }\n }\n\n return {\n pageview(): void {\n const href = env.href();\n if (href === previous || isLocal(env.hostname()) || isIgnored(href, env.device)) return;\n const referrer = previous ?? env.referrer;\n previous = href;\n env.send(endpoint, JSON.stringify({ u: href, r: referrer }));\n },\n };\n}\n","import { createTracker, readSource } from './tracker.ts';\nimport type { NakodaOptions, VisitSource } from './tracker.ts';\n\nexport type { NakodaOptions, VisitSource };\n\nfunction send(url: string, body: string): void {\n // text/plain keeps the request simple: no CORS preflight, and sendBeacon\n // outlives the page being left.\n const blob = new Blob([body], { type: 'text/plain' });\n if (navigator.sendBeacon?.(url, blob)) return;\n void fetch(url, { method: 'POST', body, keepalive: true, headers: { 'content-type': 'text/plain' } }).catch(\n () => undefined,\n );\n}\n\nfunction storage(): Storage | undefined {\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\nfunction device(): Storage | undefined {\n try {\n return window.localStorage;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Counts the page's views with nakoda, the History API's navigations\n * included, and keeps where the visit came from. Returns what stops it.\n */\nexport function init(options: NakodaOptions): () => void {\n if (typeof window === 'undefined') return () => undefined;\n const tracker = createTracker(options, {\n href: () => location.href,\n hostname: () => location.hostname,\n referrer: document.referrer,\n send,\n storage: storage(),\n device: device(),\n });\n tracker.pageview();\n\n const pushState = history.pushState;\n const replaceState = history.replaceState;\n history.pushState = function (this: History, ...args: Parameters<History['pushState']>) {\n pushState.apply(this, args);\n tracker.pageview();\n };\n history.replaceState = function (this: History, ...args: Parameters<History['replaceState']>) {\n replaceState.apply(this, args);\n tracker.pageview();\n };\n const onPop = () => tracker.pageview();\n window.addEventListener('popstate', onPop);\n\n return () => {\n history.pushState = pushState;\n history.replaceState = replaceState;\n window.removeEventListener('popstate', onPop);\n };\n}\n\n/** Where the current visit started: the landing URL and its referrer. */\nexport function getSource(): VisitSource | null {\n if (typeof window === 'undefined') return null;\n return readSource(storage());\n}\n"],"mappings":"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","names":["device","storage"]}
|
package/package.json
CHANGED