@obelism/numbers-sdk 0.1.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/README.md +31 -5
- package/dist/index.d.mts +49 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +49 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -13
- package/dist/index.mjs +124 -14
- package/dist/script.global.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ No bundler needed:
|
|
|
15
15
|
defer
|
|
16
16
|
src="https://numbers.obelism.studio/script.js"
|
|
17
17
|
data-organization="org_…"
|
|
18
|
+
data-space="space_…"
|
|
18
19
|
data-environment="production"
|
|
19
20
|
></script>
|
|
20
21
|
```
|
|
@@ -27,9 +28,17 @@ client on `window.numbers`:
|
|
|
27
28
|
numbers.track('sign_up', 'footer_form')
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
A page view is counted once per pathname: a change that only touches the
|
|
32
|
+
query string or hash (`?tab=2`, `#pricing`) isn't a second page view.
|
|
33
|
+
|
|
34
|
+
`data-space` names the space (one site or project of the organization) the
|
|
35
|
+
events belong to; without it they go to the organization's first space.
|
|
30
36
|
`data-endpoint="https://…"` sends somewhere else, e.g. a proxy on your own
|
|
31
|
-
domain
|
|
32
|
-
|
|
37
|
+
domain; without it events go to numbers' origin, even when a CDN serves the
|
|
38
|
+
file. `data-debug` turns on the client's development warnings (snake_case
|
|
39
|
+
names, an 8 KB body, a `data-space` it can't use), and
|
|
40
|
+
`data-storage="session"` (or `"none"`) counts without a cookie. npm CDNs
|
|
41
|
+
serve the same file from `@obelism/numbers-sdk/dist/script.global.js`.
|
|
33
42
|
|
|
34
43
|
## Module
|
|
35
44
|
|
|
@@ -42,13 +51,15 @@ import { createNumbers } from '@obelism/numbers-sdk'
|
|
|
42
51
|
|
|
43
52
|
const numbers = createNumbers({
|
|
44
53
|
organizationId: 'org_…',
|
|
54
|
+
spaceId: 'space_…', // optional: without it, the organization's first space
|
|
45
55
|
environment: 'production', // default; or 'develop', 'staging'
|
|
46
56
|
// endpoint: 'https://numbers.obelism.studio/api/log',
|
|
47
57
|
// disableWarnings: true,
|
|
48
58
|
})
|
|
49
59
|
|
|
50
60
|
// Page views now and on every navigation, and clicks on tagged elements.
|
|
51
|
-
const stop = numbers.autoTrack()
|
|
61
|
+
const stop = numbers.autoTrack()
|
|
62
|
+
// { pageViews?, clicks?, outbound?, downloads?, forms? }
|
|
52
63
|
|
|
53
64
|
numbers.track('add_to_cart', 'product_page', {
|
|
54
65
|
value: 59.95,
|
|
@@ -56,9 +67,16 @@ numbers.track('add_to_cart', 'product_page', {
|
|
|
56
67
|
params: { ecommerce: { items: [{ item_id: 'sku_1' }] } },
|
|
57
68
|
})
|
|
58
69
|
numbers.track('view_item', 'list', { dedupeKey: 'sku_1' }) // once per key
|
|
70
|
+
numbers.formSubmit(form, 'newsletter') // or autoTrack({ forms: true })
|
|
59
71
|
numbers.pageView() // by hand, without autoTrack
|
|
60
72
|
```
|
|
61
73
|
|
|
74
|
+
`autoTrack({ outbound: true })` sends `outbound_click` for links to another
|
|
75
|
+
site, `downloads: true` sends `file_download` for links to a file, and
|
|
76
|
+
`forms: true` sends `form_submit` for each submitted form. Calling
|
|
77
|
+
`autoTrack` again replaces the previous listeners, so it doesn't double
|
|
78
|
+
count.
|
|
79
|
+
|
|
62
80
|
The client is safe to create during server rendering: outside the browser
|
|
63
81
|
every call does nothing and returns `null`.
|
|
64
82
|
|
|
@@ -89,12 +107,20 @@ its visual editor shows each block's click rate.
|
|
|
89
107
|
|
|
90
108
|
### Visitors
|
|
91
109
|
|
|
92
|
-
A visitor is a random id in the `
|
|
110
|
+
A visitor is a random id in the `obelism_visitor_id` cookie (`visi_` and 26
|
|
93
111
|
characters, 30 days from the last visit), the same cookie the improve SDK
|
|
94
|
-
uses. `
|
|
112
|
+
uses. The old `visitorId` cookie is read and written too, for a deprecation
|
|
113
|
+
window, so a site that still runs an older improve SDK keeps one visitor.
|
|
114
|
+
`numbers.visitor()` returns `{ id, recurring }`. The device categories
|
|
95
115
|
(pointer, device, browser, os, screen size) come from the user agent; pass
|
|
96
116
|
`device` to `createNumbers` (or call `setDevice`) to use your own.
|
|
97
117
|
|
|
118
|
+
For a site that counts without asking for consent, `storage: 'session'`
|
|
119
|
+
keeps the id in sessionStorage for the tab and `storage: 'none'` makes a new
|
|
120
|
+
one on every client (so every full load in a page that isn't an SPA): neither
|
|
121
|
+
sets a cookie. The script takes the same as
|
|
122
|
+
`data-storage`.
|
|
123
|
+
|
|
98
124
|
### Limits
|
|
99
125
|
|
|
100
126
|
Strings are capped to what numbers accepts (256 characters, currency 8),
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
type Visitor = {
|
|
2
|
+
id: string;
|
|
3
|
+
/** Whether the visitor had an id already: `recurring` on the server. */
|
|
4
|
+
recurring: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Where the visitor id lives. `cookie` is the shared 30-day cookie; `session`
|
|
8
|
+
* keeps it in sessionStorage for the tab only (no cookie); `none` makes a new
|
|
9
|
+
* id for every page load, so visits still count but unique visitors don't.
|
|
10
|
+
*/
|
|
11
|
+
type VisitorStorage = 'cookie' | 'session' | 'none';
|
|
12
|
+
|
|
1
13
|
type Environment = 'develop' | 'staging' | 'production';
|
|
2
14
|
/**
|
|
3
15
|
* The visitor's device categories, as improve's audiences know them. The
|
|
@@ -12,6 +24,11 @@ type Device = {
|
|
|
12
24
|
};
|
|
13
25
|
type NumbersOptions = {
|
|
14
26
|
organizationId: string;
|
|
27
|
+
/**
|
|
28
|
+
* The space (`space_…`) the events belong to: one site or project of the
|
|
29
|
+
* organization. Without it, the organization's first space.
|
|
30
|
+
*/
|
|
31
|
+
spaceId?: string | undefined;
|
|
15
32
|
/** Default `production`. */
|
|
16
33
|
environment?: Environment;
|
|
17
34
|
/**
|
|
@@ -23,6 +40,13 @@ type NumbersOptions = {
|
|
|
23
40
|
device?: Device;
|
|
24
41
|
/** Silence the development warnings (e.g. a name that isn't snake_case). */
|
|
25
42
|
disableWarnings?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Where the visitor id lives. Default `cookie`: the shared 30-day
|
|
45
|
+
* `obelism_visitor_id` cookie. `session` keeps it in sessionStorage for the
|
|
46
|
+
* tab only, and `none` makes a new id per page load: neither sets a cookie,
|
|
47
|
+
* for a site that counts visits without asking for consent.
|
|
48
|
+
*/
|
|
49
|
+
storage?: VisitorStorage;
|
|
26
50
|
};
|
|
27
51
|
/**
|
|
28
52
|
* Event names GA4 / Google Tag Manager recommend. Reusing them lines your
|
|
@@ -60,6 +84,7 @@ type Exposure = {
|
|
|
60
84
|
type BeaconCommon<T extends 'event' | 'exposure'> = {
|
|
61
85
|
type: T;
|
|
62
86
|
organizationId: string;
|
|
87
|
+
spaceId?: string;
|
|
63
88
|
environment: Environment;
|
|
64
89
|
visitorId: string;
|
|
65
90
|
pointer: string;
|
|
@@ -86,12 +111,6 @@ type EventBeacon = BeaconCommon<'event'> & PageContext & {
|
|
|
86
111
|
};
|
|
87
112
|
type ExposureBeacon = BeaconCommon<'exposure'> & Exposure;
|
|
88
113
|
|
|
89
|
-
type Visitor = {
|
|
90
|
-
id: string;
|
|
91
|
-
/** Whether the visitor had a cookie already: `recurring` on the server. */
|
|
92
|
-
recurring: boolean;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
114
|
type AutoTrackOptions = {
|
|
96
115
|
/** A page view now and on every client-side navigation. Default on. */
|
|
97
116
|
pageViews?: boolean;
|
|
@@ -100,11 +119,27 @@ type AutoTrackOptions = {
|
|
|
100
119
|
* element with `data-numbers-id="…"`. Default on.
|
|
101
120
|
*/
|
|
102
121
|
clicks?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* An `outbound_click` for clicks on links to another site, scoped to the
|
|
124
|
+
* destination (or the link's `data-numbers-id`). Default off.
|
|
125
|
+
*/
|
|
126
|
+
outbound?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* A `file_download` for clicks on links to a file, scoped to its name (or
|
|
129
|
+
* the link's `data-numbers-id`). Default off.
|
|
130
|
+
*/
|
|
131
|
+
downloads?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* A `form_submit` for every submitted form, scoped to its
|
|
134
|
+
* `data-numbers-id`, its `id`, or its action. Default off.
|
|
135
|
+
*/
|
|
136
|
+
forms?: boolean;
|
|
103
137
|
};
|
|
104
138
|
/** What a send resolves to: the response, or nothing when it failed. */
|
|
105
139
|
type Sent = Promise<Response | undefined>;
|
|
106
140
|
type NumbersClient = {
|
|
107
141
|
readonly organizationId: string;
|
|
142
|
+
readonly spaceId: string | undefined;
|
|
108
143
|
readonly environment: Environment;
|
|
109
144
|
/** The visitor, from the cookie or new. `null` outside the browser. */
|
|
110
145
|
visitor: () => Visitor | null;
|
|
@@ -119,17 +154,21 @@ type NumbersClient = {
|
|
|
119
154
|
track: (event: EventName, scope?: string, options?: TrackOptions) => Sent | null;
|
|
120
155
|
/** Sends a page view of the current page. */
|
|
121
156
|
pageView: () => Sent | null;
|
|
157
|
+
/** Sends a `form_submit` for a form, scoped to its id or action. */
|
|
158
|
+
formSubmit: (form: HTMLFormElement, scope?: string) => Sent | null;
|
|
122
159
|
/** Sends a test's or flag's assignment, once per subject per client. */
|
|
123
160
|
expose: (exposure: Exposure) => Sent | null;
|
|
124
|
-
/** Starts
|
|
161
|
+
/** Starts automatic tracking; returns a function that stops it. */
|
|
125
162
|
autoTrack: (options?: AutoTrackOptions) => () => void;
|
|
126
163
|
};
|
|
127
|
-
declare const createNumbers: ({ organizationId, environment, endpoint, device: givenDevice, disableWarnings, }: NumbersOptions) => NumbersClient;
|
|
164
|
+
declare const createNumbers: ({ organizationId, spaceId, environment, endpoint, device: givenDevice, disableWarnings, storage, }: NumbersOptions) => NumbersClient;
|
|
128
165
|
|
|
129
166
|
declare const DEFAULT_ENDPOINT = "https://numbers.obelism.studio/api/log";
|
|
167
|
+
declare const VISITOR_COOKIE = "obelism_visitor_id";
|
|
168
|
+
declare const LEGACY_VISITOR_COOKIE = "visitorId";
|
|
130
169
|
|
|
131
170
|
declare const detectDevice: (userAgent: string) => Device;
|
|
132
171
|
|
|
133
|
-
export { DEFAULT_ENDPOINT, createNumbers, detectDevice };
|
|
134
|
-
export type { AutoTrackOptions, BeaconCommon, Device, Environment, EventBeacon, EventName, Exposure, ExposureBeacon, NumbersClient, NumbersOptions, PageContext, RecommendedEventName, Sent, TrackOptions, Visitor };
|
|
172
|
+
export { DEFAULT_ENDPOINT, LEGACY_VISITOR_COOKIE, VISITOR_COOKIE, createNumbers, detectDevice };
|
|
173
|
+
export type { AutoTrackOptions, BeaconCommon, Device, Environment, EventBeacon, EventName, Exposure, ExposureBeacon, NumbersClient, NumbersOptions, PageContext, RecommendedEventName, Sent, TrackOptions, Visitor, VisitorStorage };
|
|
135
174
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.d.mts","sources":["../src/visitor.ts","../src/types.ts","../src/client.ts","../src/constants.ts","../src/device.ts"],"mappings":"AAAA,KAAK,OAAO;AACZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;;ACTnB,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,KAAK,MAAM;AACX;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAkB,WAAW;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,oBAAoB;AACzB;AACA,KAAK,SAAS,GAAG,oBAAoB;AACrC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,QAAQ;AACb;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW,GAAG,YAAY,YAAY,WAAW;AACtD;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA,KAAK,cAAc,GAAG,YAAY,eAAe,QAAQ;;AClGzD,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,GAAG,OAAO,CAAC,QAAQ;AAC5B,KAAK,aAAa;AAClB;AACA;AACA,0BAA0B,WAAW;AACrC;AACA,mBAAmB,OAAO;AAC1B;AACA,wBAAwB,MAAM;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,SAAS,4BAA4B,YAAY,KAAK,IAAI;AAC7E;AACA,oBAAoB,IAAI;AACxB;AACA,uBAAuB,eAAe,qBAAqB,IAAI;AAC/D;AACA,uBAAuB,QAAQ,KAAK,IAAI;AACxC;AACA,0BAA0B,gBAAgB;AAC1C;AACA,cAAc,aAAa,uGAAuG,cAAc,KAAK,aAAa;;ACpDlK,cAAc,gBAAgB;AAC9B,cAAc,cAAc;AAC5B,cAAc,qBAAqB;;ACDnC,cAAc,YAAY,yBAAyB,MAAM;;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
type Visitor = {
|
|
2
|
+
id: string;
|
|
3
|
+
/** Whether the visitor had an id already: `recurring` on the server. */
|
|
4
|
+
recurring: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Where the visitor id lives. `cookie` is the shared 30-day cookie; `session`
|
|
8
|
+
* keeps it in sessionStorage for the tab only (no cookie); `none` makes a new
|
|
9
|
+
* id for every page load, so visits still count but unique visitors don't.
|
|
10
|
+
*/
|
|
11
|
+
type VisitorStorage = 'cookie' | 'session' | 'none';
|
|
12
|
+
|
|
1
13
|
type Environment = 'develop' | 'staging' | 'production';
|
|
2
14
|
/**
|
|
3
15
|
* The visitor's device categories, as improve's audiences know them. The
|
|
@@ -12,6 +24,11 @@ type Device = {
|
|
|
12
24
|
};
|
|
13
25
|
type NumbersOptions = {
|
|
14
26
|
organizationId: string;
|
|
27
|
+
/**
|
|
28
|
+
* The space (`space_…`) the events belong to: one site or project of the
|
|
29
|
+
* organization. Without it, the organization's first space.
|
|
30
|
+
*/
|
|
31
|
+
spaceId?: string | undefined;
|
|
15
32
|
/** Default `production`. */
|
|
16
33
|
environment?: Environment;
|
|
17
34
|
/**
|
|
@@ -23,6 +40,13 @@ type NumbersOptions = {
|
|
|
23
40
|
device?: Device;
|
|
24
41
|
/** Silence the development warnings (e.g. a name that isn't snake_case). */
|
|
25
42
|
disableWarnings?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Where the visitor id lives. Default `cookie`: the shared 30-day
|
|
45
|
+
* `obelism_visitor_id` cookie. `session` keeps it in sessionStorage for the
|
|
46
|
+
* tab only, and `none` makes a new id per page load: neither sets a cookie,
|
|
47
|
+
* for a site that counts visits without asking for consent.
|
|
48
|
+
*/
|
|
49
|
+
storage?: VisitorStorage;
|
|
26
50
|
};
|
|
27
51
|
/**
|
|
28
52
|
* Event names GA4 / Google Tag Manager recommend. Reusing them lines your
|
|
@@ -60,6 +84,7 @@ type Exposure = {
|
|
|
60
84
|
type BeaconCommon<T extends 'event' | 'exposure'> = {
|
|
61
85
|
type: T;
|
|
62
86
|
organizationId: string;
|
|
87
|
+
spaceId?: string;
|
|
63
88
|
environment: Environment;
|
|
64
89
|
visitorId: string;
|
|
65
90
|
pointer: string;
|
|
@@ -86,12 +111,6 @@ type EventBeacon = BeaconCommon<'event'> & PageContext & {
|
|
|
86
111
|
};
|
|
87
112
|
type ExposureBeacon = BeaconCommon<'exposure'> & Exposure;
|
|
88
113
|
|
|
89
|
-
type Visitor = {
|
|
90
|
-
id: string;
|
|
91
|
-
/** Whether the visitor had a cookie already: `recurring` on the server. */
|
|
92
|
-
recurring: boolean;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
114
|
type AutoTrackOptions = {
|
|
96
115
|
/** A page view now and on every client-side navigation. Default on. */
|
|
97
116
|
pageViews?: boolean;
|
|
@@ -100,11 +119,27 @@ type AutoTrackOptions = {
|
|
|
100
119
|
* element with `data-numbers-id="…"`. Default on.
|
|
101
120
|
*/
|
|
102
121
|
clicks?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* An `outbound_click` for clicks on links to another site, scoped to the
|
|
124
|
+
* destination (or the link's `data-numbers-id`). Default off.
|
|
125
|
+
*/
|
|
126
|
+
outbound?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* A `file_download` for clicks on links to a file, scoped to its name (or
|
|
129
|
+
* the link's `data-numbers-id`). Default off.
|
|
130
|
+
*/
|
|
131
|
+
downloads?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* A `form_submit` for every submitted form, scoped to its
|
|
134
|
+
* `data-numbers-id`, its `id`, or its action. Default off.
|
|
135
|
+
*/
|
|
136
|
+
forms?: boolean;
|
|
103
137
|
};
|
|
104
138
|
/** What a send resolves to: the response, or nothing when it failed. */
|
|
105
139
|
type Sent = Promise<Response | undefined>;
|
|
106
140
|
type NumbersClient = {
|
|
107
141
|
readonly organizationId: string;
|
|
142
|
+
readonly spaceId: string | undefined;
|
|
108
143
|
readonly environment: Environment;
|
|
109
144
|
/** The visitor, from the cookie or new. `null` outside the browser. */
|
|
110
145
|
visitor: () => Visitor | null;
|
|
@@ -119,17 +154,21 @@ type NumbersClient = {
|
|
|
119
154
|
track: (event: EventName, scope?: string, options?: TrackOptions) => Sent | null;
|
|
120
155
|
/** Sends a page view of the current page. */
|
|
121
156
|
pageView: () => Sent | null;
|
|
157
|
+
/** Sends a `form_submit` for a form, scoped to its id or action. */
|
|
158
|
+
formSubmit: (form: HTMLFormElement, scope?: string) => Sent | null;
|
|
122
159
|
/** Sends a test's or flag's assignment, once per subject per client. */
|
|
123
160
|
expose: (exposure: Exposure) => Sent | null;
|
|
124
|
-
/** Starts
|
|
161
|
+
/** Starts automatic tracking; returns a function that stops it. */
|
|
125
162
|
autoTrack: (options?: AutoTrackOptions) => () => void;
|
|
126
163
|
};
|
|
127
|
-
declare const createNumbers: ({ organizationId, environment, endpoint, device: givenDevice, disableWarnings, }: NumbersOptions) => NumbersClient;
|
|
164
|
+
declare const createNumbers: ({ organizationId, spaceId, environment, endpoint, device: givenDevice, disableWarnings, storage, }: NumbersOptions) => NumbersClient;
|
|
128
165
|
|
|
129
166
|
declare const DEFAULT_ENDPOINT = "https://numbers.obelism.studio/api/log";
|
|
167
|
+
declare const VISITOR_COOKIE = "obelism_visitor_id";
|
|
168
|
+
declare const LEGACY_VISITOR_COOKIE = "visitorId";
|
|
130
169
|
|
|
131
170
|
declare const detectDevice: (userAgent: string) => Device;
|
|
132
171
|
|
|
133
|
-
export { DEFAULT_ENDPOINT, createNumbers, detectDevice };
|
|
134
|
-
export type { AutoTrackOptions, BeaconCommon, Device, Environment, EventBeacon, EventName, Exposure, ExposureBeacon, NumbersClient, NumbersOptions, PageContext, RecommendedEventName, Sent, TrackOptions, Visitor };
|
|
172
|
+
export { DEFAULT_ENDPOINT, LEGACY_VISITOR_COOKIE, VISITOR_COOKIE, createNumbers, detectDevice };
|
|
173
|
+
export type { AutoTrackOptions, BeaconCommon, Device, Environment, EventBeacon, EventName, Exposure, ExposureBeacon, NumbersClient, NumbersOptions, PageContext, RecommendedEventName, Sent, TrackOptions, Visitor, VisitorStorage };
|
|
135
174
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../src/visitor.ts","../src/types.ts","../src/client.ts","../src/constants.ts","../src/device.ts"],"mappings":"AAAA,KAAK,OAAO;AACZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;;ACTnB,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,KAAK,MAAM;AACX;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAkB,WAAW;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,oBAAoB;AACzB;AACA,KAAK,SAAS,GAAG,oBAAoB;AACrC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,QAAQ;AACb;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW,GAAG,YAAY,YAAY,WAAW;AACtD;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA,KAAK,cAAc,GAAG,YAAY,eAAe,QAAQ;;AClGzD,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,GAAG,OAAO,CAAC,QAAQ;AAC5B,KAAK,aAAa;AAClB;AACA;AACA,0BAA0B,WAAW;AACrC;AACA,mBAAmB,OAAO;AAC1B;AACA,wBAAwB,MAAM;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,SAAS,4BAA4B,YAAY,KAAK,IAAI;AAC7E;AACA,oBAAoB,IAAI;AACxB;AACA,uBAAuB,eAAe,qBAAqB,IAAI;AAC/D;AACA,uBAAuB,QAAQ,KAAK,IAAI;AACxC;AACA,0BAA0B,gBAAgB;AAC1C;AACA,cAAc,aAAa,uGAAuG,cAAc,KAAK,aAAa;;ACpDlK,cAAc,gBAAgB;AAC9B,cAAc,cAAc;AAC5B,cAAc,qBAAqB;;ACDnC,cAAc,YAAY,yBAAyB,MAAM;;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,17 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_ENDPOINT = 'https://numbers.obelism.studio/api/log';
|
|
4
4
|
// The visitor cookie and id format, shared with the improve SDK so a visitor
|
|
5
|
-
// is the same one in both: `visi_` and 26 characters.
|
|
6
|
-
|
|
5
|
+
// is the same one in both: `visi_` and 26 characters. The name is namespaced
|
|
6
|
+
// so a consent declaration can name it without colliding with other analytics;
|
|
7
|
+
// `visitorId` is the old name, still read so existing visitors keep their id.
|
|
8
|
+
const VISITOR_COOKIE = 'obelism_visitor_id';
|
|
9
|
+
const LEGACY_VISITOR_COOKIE = 'visitorId';
|
|
7
10
|
const VISITOR_ID_PREFIX = 'visi';
|
|
8
11
|
const VISITOR_ID_SEPARATOR = '_';
|
|
9
12
|
const VISITOR_ID_LENGTH = 26;
|
|
10
13
|
const VISITOR_COOKIE_DAYS = 30;
|
|
14
|
+
// The sessionStorage key for the session-only visitor (no cookie).
|
|
15
|
+
const VISITOR_SESSION_KEY = 'obelism_visitor_id';
|
|
11
16
|
// numbers' limits (`POST /api/log`): a longer string is a 400 and a bigger
|
|
12
17
|
// body a 413, and either loses the event, so the client caps what it can.
|
|
13
18
|
const MAX_FIELD_LENGTH = 256;
|
|
@@ -90,19 +95,73 @@ const createVisitorId = ()=>{
|
|
|
90
95
|
for (const byte of random)id += CHARACTERS[byte % CHARACTERS.length];
|
|
91
96
|
return `${VISITOR_ID_PREFIX}${VISITOR_ID_SEPARATOR}${id}`;
|
|
92
97
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const readStoredVisitor = (storage)=>{
|
|
99
|
+
if (storage === 'session') {
|
|
100
|
+
try {
|
|
101
|
+
return sessionStorage.getItem(VISITOR_SESSION_KEY) ?? '';
|
|
102
|
+
} catch {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return readCookie(VISITOR_COOKIE) || readCookie(LEGACY_VISITOR_COOKIE);
|
|
107
|
+
};
|
|
108
|
+
const writeStoredVisitor = (storage, id)=>{
|
|
109
|
+
if (storage === 'session') {
|
|
110
|
+
try {
|
|
111
|
+
sessionStorage.setItem(VISITOR_SESSION_KEY, id);
|
|
112
|
+
} catch {
|
|
113
|
+
// Storage can be disabled; the id then lives for this call only.
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
writeCookie(VISITOR_COOKIE, id);
|
|
118
|
+
// Write the old cookie too, for a deprecation window: a site that still
|
|
119
|
+
// runs an older improve SDK reads only `visitorId` and would mint a second
|
|
120
|
+
// id for the same person. Drop this with the legacy read.
|
|
121
|
+
writeCookie(LEGACY_VISITOR_COOKIE, id);
|
|
122
|
+
};
|
|
123
|
+
// The visitor in its storage, or a new one; either way it is written again, so
|
|
124
|
+
// the cookie lasts 30 days from the last visit and the session id lasts the tab.
|
|
125
|
+
const loadVisitor = (storage = 'cookie')=>{
|
|
126
|
+
if (storage === 'none') return {
|
|
127
|
+
id: createVisitorId(),
|
|
128
|
+
recurring: false
|
|
129
|
+
};
|
|
130
|
+
const existing = readStoredVisitor(storage);
|
|
97
131
|
const recurring = isVisitorId(existing);
|
|
98
132
|
const id = recurring ? existing : createVisitorId();
|
|
99
|
-
|
|
133
|
+
writeStoredVisitor(storage, id);
|
|
100
134
|
return {
|
|
101
135
|
id,
|
|
102
136
|
recurring
|
|
103
137
|
};
|
|
104
138
|
};
|
|
105
139
|
|
|
140
|
+
// Documents, archives and media, not images a browser shows inline (a link
|
|
141
|
+
// with `download` still counts, whatever it points at).
|
|
142
|
+
const DOWNLOAD_EXTENSIONS = [
|
|
143
|
+
'7z',
|
|
144
|
+
'avi',
|
|
145
|
+
'csv',
|
|
146
|
+
'doc',
|
|
147
|
+
'docx',
|
|
148
|
+
'gz',
|
|
149
|
+
'mov',
|
|
150
|
+
'mp3',
|
|
151
|
+
'mp4',
|
|
152
|
+
'pdf',
|
|
153
|
+
'ppt',
|
|
154
|
+
'pptx',
|
|
155
|
+
'rar',
|
|
156
|
+
'tar',
|
|
157
|
+
'txt',
|
|
158
|
+
'wav',
|
|
159
|
+
'xls',
|
|
160
|
+
'xlsx',
|
|
161
|
+
'xml',
|
|
162
|
+
'zip'
|
|
163
|
+
];
|
|
164
|
+
const isDownload = (url, link)=>link.hasAttribute('download') || DOWNLOAD_EXTENSIONS.includes(url.pathname.split('.').pop()?.toLowerCase() ?? '');
|
|
106
165
|
const SNAKE_CASE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|
|
107
166
|
const parseRetryAfterMs = (header)=>{
|
|
108
167
|
if (!header) return undefined;
|
|
@@ -112,13 +171,14 @@ const parseRetryAfterMs = (header)=>{
|
|
|
112
171
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
113
172
|
};
|
|
114
173
|
const isBrowser = ()=>typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
115
|
-
const createNumbers = ({ organizationId, environment = 'production', endpoint = DEFAULT_ENDPOINT, device: givenDevice, disableWarnings = false })=>{
|
|
174
|
+
const createNumbers = ({ organizationId, spaceId, environment = 'production', endpoint = DEFAULT_ENDPOINT, device: givenDevice, disableWarnings = false, storage = 'cookie' })=>{
|
|
116
175
|
let visitor = null;
|
|
117
176
|
let device = givenDevice;
|
|
118
177
|
let url = endpoint;
|
|
119
178
|
// Epoch ms until which sending is paused after a 429.
|
|
120
179
|
let pausedUntil = 0;
|
|
121
180
|
let firstPageView = true;
|
|
181
|
+
let stopAutoTrack = null;
|
|
122
182
|
const sentKeys = new Set();
|
|
123
183
|
const exposed = new Set();
|
|
124
184
|
const warned = new Set();
|
|
@@ -129,7 +189,7 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
129
189
|
};
|
|
130
190
|
const getVisitor = ()=>{
|
|
131
191
|
if (!isBrowser()) return null;
|
|
132
|
-
visitor ??= loadVisitor();
|
|
192
|
+
visitor ??= loadVisitor(storage);
|
|
133
193
|
return visitor;
|
|
134
194
|
};
|
|
135
195
|
const common = (type, current)=>{
|
|
@@ -137,6 +197,9 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
137
197
|
return {
|
|
138
198
|
type,
|
|
139
199
|
organizationId,
|
|
200
|
+
...spaceId && {
|
|
201
|
+
spaceId
|
|
202
|
+
},
|
|
140
203
|
environment,
|
|
141
204
|
visitorId: current.id,
|
|
142
205
|
pointer: device.pointer,
|
|
@@ -153,8 +216,10 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
153
216
|
if (typeof TextEncoder !== 'undefined' && new TextEncoder().encode(serialized).length > MAX_BODY_BYTES) warn(`size:${label}`, `"${label}" is over ${MAX_BODY_BYTES} bytes and will be refused: trim its \`params\`.`);
|
|
154
217
|
return fetch(url, {
|
|
155
218
|
method: 'POST',
|
|
219
|
+
// A CORS-safelisted type, so a browser sends the event without a
|
|
220
|
+
// preflight; numbers parses the body as JSON either way.
|
|
156
221
|
headers: {
|
|
157
|
-
'Content-Type': '
|
|
222
|
+
'Content-Type': 'text/plain;charset=UTF-8'
|
|
158
223
|
},
|
|
159
224
|
body: serialized,
|
|
160
225
|
keepalive: true
|
|
@@ -200,6 +265,17 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
200
265
|
firstPageView = false;
|
|
201
266
|
return sendEvent('page_view', location.pathname, {}, referrer);
|
|
202
267
|
};
|
|
268
|
+
const formScope = (form)=>{
|
|
269
|
+
const given = form.getAttribute('data-numbers-id') || form.id;
|
|
270
|
+
if (given) return given;
|
|
271
|
+
try {
|
|
272
|
+
const action = new URL(form.action, location.href);
|
|
273
|
+
return action.origin === location.origin ? action.pathname : `${action.origin}${action.pathname}`;
|
|
274
|
+
} catch {
|
|
275
|
+
return 'form';
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const formSubmit = (form, scope)=>track('form_submit', scope || formScope(form));
|
|
203
279
|
const expose = (exposure)=>{
|
|
204
280
|
const current = getVisitor();
|
|
205
281
|
if (!current || paused()) return null;
|
|
@@ -211,8 +287,11 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
211
287
|
...exposure
|
|
212
288
|
}, key);
|
|
213
289
|
};
|
|
214
|
-
const autoTrack = ({ pageViews = true, clicks = true } = {})=>{
|
|
290
|
+
const autoTrack = ({ pageViews = true, clicks = true, outbound = false, downloads = false, forms = false } = {})=>{
|
|
215
291
|
if (!isBrowser()) return ()=>{};
|
|
292
|
+
// A second call replaces the first instead of stacking listeners, so a
|
|
293
|
+
// site can add outbound/download/form tracking to the script's autoTrack.
|
|
294
|
+
stopAutoTrack?.();
|
|
216
295
|
const stops = [];
|
|
217
296
|
if (pageViews) {
|
|
218
297
|
// One page view per path: replaceState for a query or hash change
|
|
@@ -252,12 +331,42 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
252
331
|
document.addEventListener('click', onClick, true);
|
|
253
332
|
stops.push(()=>document.removeEventListener('click', onClick, true));
|
|
254
333
|
}
|
|
255
|
-
|
|
256
|
-
|
|
334
|
+
if (outbound || downloads) {
|
|
335
|
+
const onLinkClick = (event)=>{
|
|
336
|
+
const target = event.target;
|
|
337
|
+
const link = target?.closest?.('a[href]');
|
|
338
|
+
if (!link) return;
|
|
339
|
+
let url;
|
|
340
|
+
try {
|
|
341
|
+
url = new URL(link.href, location.href);
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const id = link.closest('[data-numbers-id]')?.getAttribute('data-numbers-id');
|
|
346
|
+
if (downloads && isDownload(url, link)) track('file_download', id ?? url.pathname.split('/').pop() ?? url.pathname);
|
|
347
|
+
else if (outbound && url.origin !== location.origin && /^https?:$/.test(url.protocol)) track('outbound_click', id ?? `${url.origin}${url.pathname}`);
|
|
348
|
+
};
|
|
349
|
+
document.addEventListener('click', onLinkClick, true);
|
|
350
|
+
stops.push(()=>document.removeEventListener('click', onLinkClick, true));
|
|
351
|
+
}
|
|
352
|
+
if (forms) {
|
|
353
|
+
const onSubmit = (event)=>{
|
|
354
|
+
const form = event.target;
|
|
355
|
+
if (form instanceof HTMLFormElement) formSubmit(form);
|
|
356
|
+
};
|
|
357
|
+
document.addEventListener('submit', onSubmit, true);
|
|
358
|
+
stops.push(()=>document.removeEventListener('submit', onSubmit, true));
|
|
359
|
+
}
|
|
360
|
+
const stop = ()=>{
|
|
361
|
+
for (const fn of stops)fn();
|
|
362
|
+
if (stopAutoTrack === stop) stopAutoTrack = null;
|
|
257
363
|
};
|
|
364
|
+
stopAutoTrack = stop;
|
|
365
|
+
return stop;
|
|
258
366
|
};
|
|
259
367
|
return {
|
|
260
368
|
organizationId,
|
|
369
|
+
spaceId,
|
|
261
370
|
environment,
|
|
262
371
|
visitor: getVisitor,
|
|
263
372
|
setDevice: (next)=>{
|
|
@@ -268,11 +377,14 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
268
377
|
},
|
|
269
378
|
track,
|
|
270
379
|
pageView,
|
|
380
|
+
formSubmit,
|
|
271
381
|
expose,
|
|
272
382
|
autoTrack
|
|
273
383
|
};
|
|
274
384
|
};
|
|
275
385
|
|
|
276
386
|
exports.DEFAULT_ENDPOINT = DEFAULT_ENDPOINT;
|
|
387
|
+
exports.LEGACY_VISITOR_COOKIE = LEGACY_VISITOR_COOKIE;
|
|
388
|
+
exports.VISITOR_COOKIE = VISITOR_COOKIE;
|
|
277
389
|
exports.createNumbers = createNumbers;
|
|
278
390
|
exports.detectDevice = detectDevice;
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const DEFAULT_ENDPOINT = 'https://numbers.obelism.studio/api/log';
|
|
2
2
|
// The visitor cookie and id format, shared with the improve SDK so a visitor
|
|
3
|
-
// is the same one in both: `visi_` and 26 characters.
|
|
4
|
-
|
|
3
|
+
// is the same one in both: `visi_` and 26 characters. The name is namespaced
|
|
4
|
+
// so a consent declaration can name it without colliding with other analytics;
|
|
5
|
+
// `visitorId` is the old name, still read so existing visitors keep their id.
|
|
6
|
+
const VISITOR_COOKIE = 'obelism_visitor_id';
|
|
7
|
+
const LEGACY_VISITOR_COOKIE = 'visitorId';
|
|
5
8
|
const VISITOR_ID_PREFIX = 'visi';
|
|
6
9
|
const VISITOR_ID_SEPARATOR = '_';
|
|
7
10
|
const VISITOR_ID_LENGTH = 26;
|
|
8
11
|
const VISITOR_COOKIE_DAYS = 30;
|
|
12
|
+
// The sessionStorage key for the session-only visitor (no cookie).
|
|
13
|
+
const VISITOR_SESSION_KEY = 'obelism_visitor_id';
|
|
9
14
|
// numbers' limits (`POST /api/log`): a longer string is a 400 and a bigger
|
|
10
15
|
// body a 413, and either loses the event, so the client caps what it can.
|
|
11
16
|
const MAX_FIELD_LENGTH = 256;
|
|
@@ -88,19 +93,73 @@ const createVisitorId = ()=>{
|
|
|
88
93
|
for (const byte of random)id += CHARACTERS[byte % CHARACTERS.length];
|
|
89
94
|
return `${VISITOR_ID_PREFIX}${VISITOR_ID_SEPARATOR}${id}`;
|
|
90
95
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const readStoredVisitor = (storage)=>{
|
|
97
|
+
if (storage === 'session') {
|
|
98
|
+
try {
|
|
99
|
+
return sessionStorage.getItem(VISITOR_SESSION_KEY) ?? '';
|
|
100
|
+
} catch {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return readCookie(VISITOR_COOKIE) || readCookie(LEGACY_VISITOR_COOKIE);
|
|
105
|
+
};
|
|
106
|
+
const writeStoredVisitor = (storage, id)=>{
|
|
107
|
+
if (storage === 'session') {
|
|
108
|
+
try {
|
|
109
|
+
sessionStorage.setItem(VISITOR_SESSION_KEY, id);
|
|
110
|
+
} catch {
|
|
111
|
+
// Storage can be disabled; the id then lives for this call only.
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
writeCookie(VISITOR_COOKIE, id);
|
|
116
|
+
// Write the old cookie too, for a deprecation window: a site that still
|
|
117
|
+
// runs an older improve SDK reads only `visitorId` and would mint a second
|
|
118
|
+
// id for the same person. Drop this with the legacy read.
|
|
119
|
+
writeCookie(LEGACY_VISITOR_COOKIE, id);
|
|
120
|
+
};
|
|
121
|
+
// The visitor in its storage, or a new one; either way it is written again, so
|
|
122
|
+
// the cookie lasts 30 days from the last visit and the session id lasts the tab.
|
|
123
|
+
const loadVisitor = (storage = 'cookie')=>{
|
|
124
|
+
if (storage === 'none') return {
|
|
125
|
+
id: createVisitorId(),
|
|
126
|
+
recurring: false
|
|
127
|
+
};
|
|
128
|
+
const existing = readStoredVisitor(storage);
|
|
95
129
|
const recurring = isVisitorId(existing);
|
|
96
130
|
const id = recurring ? existing : createVisitorId();
|
|
97
|
-
|
|
131
|
+
writeStoredVisitor(storage, id);
|
|
98
132
|
return {
|
|
99
133
|
id,
|
|
100
134
|
recurring
|
|
101
135
|
};
|
|
102
136
|
};
|
|
103
137
|
|
|
138
|
+
// Documents, archives and media, not images a browser shows inline (a link
|
|
139
|
+
// with `download` still counts, whatever it points at).
|
|
140
|
+
const DOWNLOAD_EXTENSIONS = [
|
|
141
|
+
'7z',
|
|
142
|
+
'avi',
|
|
143
|
+
'csv',
|
|
144
|
+
'doc',
|
|
145
|
+
'docx',
|
|
146
|
+
'gz',
|
|
147
|
+
'mov',
|
|
148
|
+
'mp3',
|
|
149
|
+
'mp4',
|
|
150
|
+
'pdf',
|
|
151
|
+
'ppt',
|
|
152
|
+
'pptx',
|
|
153
|
+
'rar',
|
|
154
|
+
'tar',
|
|
155
|
+
'txt',
|
|
156
|
+
'wav',
|
|
157
|
+
'xls',
|
|
158
|
+
'xlsx',
|
|
159
|
+
'xml',
|
|
160
|
+
'zip'
|
|
161
|
+
];
|
|
162
|
+
const isDownload = (url, link)=>link.hasAttribute('download') || DOWNLOAD_EXTENSIONS.includes(url.pathname.split('.').pop()?.toLowerCase() ?? '');
|
|
104
163
|
const SNAKE_CASE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|
|
105
164
|
const parseRetryAfterMs = (header)=>{
|
|
106
165
|
if (!header) return undefined;
|
|
@@ -110,13 +169,14 @@ const parseRetryAfterMs = (header)=>{
|
|
|
110
169
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
111
170
|
};
|
|
112
171
|
const isBrowser = ()=>typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
113
|
-
const createNumbers = ({ organizationId, environment = 'production', endpoint = DEFAULT_ENDPOINT, device: givenDevice, disableWarnings = false })=>{
|
|
172
|
+
const createNumbers = ({ organizationId, spaceId, environment = 'production', endpoint = DEFAULT_ENDPOINT, device: givenDevice, disableWarnings = false, storage = 'cookie' })=>{
|
|
114
173
|
let visitor = null;
|
|
115
174
|
let device = givenDevice;
|
|
116
175
|
let url = endpoint;
|
|
117
176
|
// Epoch ms until which sending is paused after a 429.
|
|
118
177
|
let pausedUntil = 0;
|
|
119
178
|
let firstPageView = true;
|
|
179
|
+
let stopAutoTrack = null;
|
|
120
180
|
const sentKeys = new Set();
|
|
121
181
|
const exposed = new Set();
|
|
122
182
|
const warned = new Set();
|
|
@@ -127,7 +187,7 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
127
187
|
};
|
|
128
188
|
const getVisitor = ()=>{
|
|
129
189
|
if (!isBrowser()) return null;
|
|
130
|
-
visitor ??= loadVisitor();
|
|
190
|
+
visitor ??= loadVisitor(storage);
|
|
131
191
|
return visitor;
|
|
132
192
|
};
|
|
133
193
|
const common = (type, current)=>{
|
|
@@ -135,6 +195,9 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
135
195
|
return {
|
|
136
196
|
type,
|
|
137
197
|
organizationId,
|
|
198
|
+
...spaceId && {
|
|
199
|
+
spaceId
|
|
200
|
+
},
|
|
138
201
|
environment,
|
|
139
202
|
visitorId: current.id,
|
|
140
203
|
pointer: device.pointer,
|
|
@@ -151,8 +214,10 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
151
214
|
if (typeof TextEncoder !== 'undefined' && new TextEncoder().encode(serialized).length > MAX_BODY_BYTES) warn(`size:${label}`, `"${label}" is over ${MAX_BODY_BYTES} bytes and will be refused: trim its \`params\`.`);
|
|
152
215
|
return fetch(url, {
|
|
153
216
|
method: 'POST',
|
|
217
|
+
// A CORS-safelisted type, so a browser sends the event without a
|
|
218
|
+
// preflight; numbers parses the body as JSON either way.
|
|
154
219
|
headers: {
|
|
155
|
-
'Content-Type': '
|
|
220
|
+
'Content-Type': 'text/plain;charset=UTF-8'
|
|
156
221
|
},
|
|
157
222
|
body: serialized,
|
|
158
223
|
keepalive: true
|
|
@@ -198,6 +263,17 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
198
263
|
firstPageView = false;
|
|
199
264
|
return sendEvent('page_view', location.pathname, {}, referrer);
|
|
200
265
|
};
|
|
266
|
+
const formScope = (form)=>{
|
|
267
|
+
const given = form.getAttribute('data-numbers-id') || form.id;
|
|
268
|
+
if (given) return given;
|
|
269
|
+
try {
|
|
270
|
+
const action = new URL(form.action, location.href);
|
|
271
|
+
return action.origin === location.origin ? action.pathname : `${action.origin}${action.pathname}`;
|
|
272
|
+
} catch {
|
|
273
|
+
return 'form';
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const formSubmit = (form, scope)=>track('form_submit', scope || formScope(form));
|
|
201
277
|
const expose = (exposure)=>{
|
|
202
278
|
const current = getVisitor();
|
|
203
279
|
if (!current || paused()) return null;
|
|
@@ -209,8 +285,11 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
209
285
|
...exposure
|
|
210
286
|
}, key);
|
|
211
287
|
};
|
|
212
|
-
const autoTrack = ({ pageViews = true, clicks = true } = {})=>{
|
|
288
|
+
const autoTrack = ({ pageViews = true, clicks = true, outbound = false, downloads = false, forms = false } = {})=>{
|
|
213
289
|
if (!isBrowser()) return ()=>{};
|
|
290
|
+
// A second call replaces the first instead of stacking listeners, so a
|
|
291
|
+
// site can add outbound/download/form tracking to the script's autoTrack.
|
|
292
|
+
stopAutoTrack?.();
|
|
214
293
|
const stops = [];
|
|
215
294
|
if (pageViews) {
|
|
216
295
|
// One page view per path: replaceState for a query or hash change
|
|
@@ -250,12 +329,42 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
250
329
|
document.addEventListener('click', onClick, true);
|
|
251
330
|
stops.push(()=>document.removeEventListener('click', onClick, true));
|
|
252
331
|
}
|
|
253
|
-
|
|
254
|
-
|
|
332
|
+
if (outbound || downloads) {
|
|
333
|
+
const onLinkClick = (event)=>{
|
|
334
|
+
const target = event.target;
|
|
335
|
+
const link = target?.closest?.('a[href]');
|
|
336
|
+
if (!link) return;
|
|
337
|
+
let url;
|
|
338
|
+
try {
|
|
339
|
+
url = new URL(link.href, location.href);
|
|
340
|
+
} catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const id = link.closest('[data-numbers-id]')?.getAttribute('data-numbers-id');
|
|
344
|
+
if (downloads && isDownload(url, link)) track('file_download', id ?? url.pathname.split('/').pop() ?? url.pathname);
|
|
345
|
+
else if (outbound && url.origin !== location.origin && /^https?:$/.test(url.protocol)) track('outbound_click', id ?? `${url.origin}${url.pathname}`);
|
|
346
|
+
};
|
|
347
|
+
document.addEventListener('click', onLinkClick, true);
|
|
348
|
+
stops.push(()=>document.removeEventListener('click', onLinkClick, true));
|
|
349
|
+
}
|
|
350
|
+
if (forms) {
|
|
351
|
+
const onSubmit = (event)=>{
|
|
352
|
+
const form = event.target;
|
|
353
|
+
if (form instanceof HTMLFormElement) formSubmit(form);
|
|
354
|
+
};
|
|
355
|
+
document.addEventListener('submit', onSubmit, true);
|
|
356
|
+
stops.push(()=>document.removeEventListener('submit', onSubmit, true));
|
|
357
|
+
}
|
|
358
|
+
const stop = ()=>{
|
|
359
|
+
for (const fn of stops)fn();
|
|
360
|
+
if (stopAutoTrack === stop) stopAutoTrack = null;
|
|
255
361
|
};
|
|
362
|
+
stopAutoTrack = stop;
|
|
363
|
+
return stop;
|
|
256
364
|
};
|
|
257
365
|
return {
|
|
258
366
|
organizationId,
|
|
367
|
+
spaceId,
|
|
259
368
|
environment,
|
|
260
369
|
visitor: getVisitor,
|
|
261
370
|
setDevice: (next)=>{
|
|
@@ -266,9 +375,10 @@ const createNumbers = ({ organizationId, environment = 'production', endpoint =
|
|
|
266
375
|
},
|
|
267
376
|
track,
|
|
268
377
|
pageView,
|
|
378
|
+
formSubmit,
|
|
269
379
|
expose,
|
|
270
380
|
autoTrack
|
|
271
381
|
};
|
|
272
382
|
};
|
|
273
383
|
|
|
274
|
-
export { DEFAULT_ENDPOINT, createNumbers, detectDevice };
|
|
384
|
+
export { DEFAULT_ENDPOINT, LEGACY_VISITOR_COOKIE, VISITOR_COOKIE, createNumbers, detectDevice };
|
package/dist/script.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!/*! Obelism numbers (@obelism/numbers-sdk), MIT. https://numbers.obelism.studio */function(){"use strict";let e="https://numbers.obelism.studio/api/log",t="visitorId",r="visi",
|
|
1
|
+
!/*! Obelism numbers (@obelism/numbers-sdk), MIT. https://numbers.obelism.studio */function(){"use strict";let e="https://numbers.obelism.studio/api/log",t="obelism_visitor_id",n="visitorId",r="visi",o="obelism_visitor_id",i=(e,t)=>e.get(t)?.slice(0,256)||void 0,s="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",a=e=>{let t=document.cookie.split("; ").find(t=>t.split("=")[0]===e);return t?t.split("=")[1]??"":""},l=(e,t)=>{let n=new Date(Date.now()+2592e6);document.cookie=`${e}=${t};path=/;expires=${n.toUTCString()};SameSite=Lax`+("https:"===location.protocol?";Secure":"")},u=()=>{let e=new Uint8Array(26);crypto.getRandomValues(e);let t="";for(let n of e)t+=s[n%s.length];return`${r}_${t}`},c=["7z","avi","csv","doc","docx","gz","mov","mp3","mp4","pdf","ppt","pptx","rar","tar","txt","wav","xls","xlsx","xml","zip"],d=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,m=()=>"u">typeof window&&"u">typeof document;((s,p)=>{var h;if(!p||s.numbers)return;let f=p.dataset.organization;if(!f)return;let g=void 0!==p.dataset.debug;!f.startsWith("org_")&&"u">typeof console&&console.warn(`[numbers] data-organization "${f}" is not an org_… id.`);let b=p.dataset.space,w=b&&b.startsWith("space_")?b:void 0;b&&!w&&"u">typeof console&&console.warn(`[numbers] data-space "${b}" is not a space_… id and will be ignored.`);let v=p.dataset.storage;v&&"session"!==v&&"none"!==v&&(h=`data-storage "${v}" is not "session" or "none"; using a cookie.`,g&&"u">typeof console&&console.warn(`[numbers] ${h}`));let y=(({organizationId:s,spaceId:p,environment:h="production",endpoint:f=e,device:g,disableWarnings:b=!1,storage:w="cookie"})=>{let v=null,y=g,$=f,_=0,k=!0,x=null,L=new Set,S=new Set,E=new Set,A=(e,t)=>{!(b||E.has(e))&&(E.add(e),"u">typeof console&&console.warn(`[numbers] ${t}`))},T=()=>m()?(v??(v=((e="cookie")=>{let i;if("none"===e)return{id:u(),recurring:!1};let s=(e=>{if("session"===e)try{return sessionStorage.getItem(o)??""}catch{return""}return a(t)||a(n)})(e),c=2===(i=s.split("_")).length&&i[0]===r&&26===i[1].length,d=c?s:u();return((e,r)=>{if("session"===e){try{sessionStorage.setItem(o,r)}catch{}return}l(t,r),l(n,r)})(e,d),{id:d,recurring:c}})(w)),v):null,z=(e,t)=>{let n,r,o;return y??(n=navigator.userAgent.toLowerCase(),r=/ipad|tablet|android(?!.*mobile)/.test(n)?"tablet":/mobi|iphone|ipod|android/.test(n)?"mobile":"desktop",y={pointer:"desktop"===r?"fine":"coarse",device:r,browser:/edg\//.test(n)?"edge":/samsungbrowser/.test(n)?"samsung internet":/instagram|fban|fbav|tiktok|snapchat|wechat|line\//.test(n)?"social":/firefox|fxios/.test(n)?"firefox":/chrome|crios|chromium/.test(n)?"chrome":/safari/.test(n)?"safari":"other",os:/iphone|ipad|ipod/.test(n)?"ios":/android/.test(n)?"android":/windows/.test(n)?"windows":/mac os/.test(n)?"mac os":"unix"}),{type:e,organizationId:s,...p&&{spaceId:p},environment:h,visitorId:t.id,pointer:y.pointer,device:y.device,screen:(o=window.innerWidth)<=768?"small":o<=1024?"medium":o<=1200?"large":"huge",browser:y.browser,os:y.os,visitor:t.recurring?"recurring":"new"}},D=(e,t)=>{let n=JSON.stringify(e);return"u">typeof TextEncoder&&new TextEncoder().encode(n).length>8192&&A(`size:${t}`,`"${t}" is over 8192 bytes and will be refused: trim its \`params\`.`),fetch($,{method:"POST",headers:{"Content-Type":"text/plain;charset=UTF-8"},body:n,keepalive:!0}).then(e=>(429===e.status&&(_=Date.now()+((e=>{if(!e)return;let t=Number(e);if(Number.isFinite(t))return Math.max(0,1e3*t);let n=Date.parse(e);return Number.isNaN(n)?void 0:Math.max(0,n-Date.now())})(e.headers.get("Retry-After"))??6e4)),e),()=>void 0)},U=(e,t,n,r)=>{let o,s,a,l,u,c=T();if(!c||Date.now()<_)return null;let{value:d,currency:m,params:p,dedupeKey:h}=n;if(void 0!==h){let t=`${e}:${h}`;if(L.has(t))return null;L.add(t)}let f={...z("event",c),...(o=new URLSearchParams(location.search),s={path:location.pathname.slice(0,1024)},a=i(o,"utm_source"),l=i(o,"utm_medium"),u=i(o,"utm_campaign"),a&&(s.utmSource=a),l&&(s.utmMedium=l),u&&(s.utmCampaign=u),s),event:e.slice(0,256),scope:t.slice(0,256)};return r&&(f.referrer=r),"number"==typeof d&&Number.isFinite(d)&&(f.value=d),m&&(f.currency=m.slice(0,8)),p&&"object"==typeof p&&!Array.isArray(p)&&(f.params=p),D(f,e)},N=(e,t,n={})=>m()?(d.test(e)||A(`name:${e}`,`"${e}" isn't snake_case. Prefer GA4-style names like "add_to_cart" or "sign_up"; \`disableWarnings: true\` silences this.`),U(e,t||location.pathname,n)):null,R=()=>{if(!m())return null;let e=k?(()=>{if(document.referrer)try{let e=new URL(document.referrer);if(e.host===location.host)return;return`${e.origin}${e.pathname}`.slice(0,1024)}catch{return}})():void 0;return k=!1,U("page_view",location.pathname,{},e)},C=(e,t)=>N("form_submit",t||(e=>{let t=e.getAttribute("data-numbers-id")||e.id;if(t)return t;try{let t=new URL(e.action,location.href);return t.origin===location.origin?t.pathname:`${t.origin}${t.pathname}`}catch{return"form"}})(e));return{organizationId:s,spaceId:p,environment:h,visitor:T,setDevice:e=>{y=e},setEndpoint:e=>{$=e},track:N,pageView:R,formSubmit:C,expose:e=>{let t=T();if(!t||Date.now()<_)return null;let n=`${e.subjectKind}:${e.subjectId}`;return S.has(n)?null:(S.add(n),D({...z("exposure",t),...e},n))},autoTrack:({pageViews:e=!0,clicks:t=!0,outbound:n=!1,downloads:r=!1,forms:o=!1}={})=>{if(!m())return()=>{};x?.();let i=[];if(e){let e=null,t=()=>{location.pathname!==e&&(e=location.pathname,R())},n=e=>{let n=history[e],r=function(...e){let r=n.apply(this,e);return t(),r};history[e]=r,i.push(()=>{history[e]===r&&(history[e]=n)})};n("pushState"),n("replaceState"),window.addEventListener("popstate",t),i.push(()=>window.removeEventListener("popstate",t)),t()}if(t){let e=e=>{let t=e.target,n=t?.closest?.("[data-numbers-id]")?.getAttribute("data-numbers-id");n&&N("click",n)};document.addEventListener("click",e,!0),i.push(()=>document.removeEventListener("click",e,!0))}if(n||r){let e=e=>{let t,o,i=e.target,s=i?.closest?.("a[href]");if(!s)return;try{t=new URL(s.href,location.href)}catch{return}let a=s.closest("[data-numbers-id]")?.getAttribute("data-numbers-id");r&&(o=t,s.hasAttribute("download")||c.includes(o.pathname.split(".").pop()?.toLowerCase()??""))?N("file_download",a??t.pathname.split("/").pop()??t.pathname):n&&t.origin!==location.origin&&/^https?:$/.test(t.protocol)&&N("outbound_click",a??`${t.origin}${t.pathname}`)};document.addEventListener("click",e,!0),i.push(()=>document.removeEventListener("click",e,!0))}if(o){let e=e=>{let t=e.target;t instanceof HTMLFormElement&&C(t)};document.addEventListener("submit",e,!0),i.push(()=>document.removeEventListener("submit",e,!0))}let s=()=>{for(let e of i)e();x===s&&(x=null)};return x=s,s}}})({organizationId:f,spaceId:w,environment:p.dataset.environment||"production",endpoint:p.dataset.endpoint||(t=>{let n;try{n=new URL(t)}catch{return e}let r="localhost"===n.hostname||"127.0.0.1"===n.hostname;return"numbers.obelism.studio"===n.hostname||r?`${n.origin}/api/log`:e})(p.src),disableWarnings:!g,storage:"session"===v||"none"===v?v:"cookie"});return s.numbers=y,y.autoTrack()})(window,document.currentScript)}();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obelism/numbers-sdk",
|
|
3
3
|
"description": "Obelism numbers SDK: page views, events and clicks on tagged elements, sent to numbers.obelism.studio",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"analytics",
|
|
7
7
|
"events",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
49
49
|
"@typescript/typescript6": "^6.0.2",
|
|
50
50
|
"bunchee": "^7.0.2",
|
|
51
|
-
"jsdom": "^30.1.
|
|
52
|
-
"npm-run-all2": "^9.0.
|
|
53
|
-
"oxlint": "^1.
|
|
54
|
-
"rollup": "^4.
|
|
51
|
+
"jsdom": "^30.1.3",
|
|
52
|
+
"npm-run-all2": "^9.0.4",
|
|
53
|
+
"oxlint": "^1.90.0",
|
|
54
|
+
"rollup": "^4.65.1",
|
|
55
55
|
"rollup-plugin-swc3": "^0.12.1",
|
|
56
56
|
"typescript": "^7.0.2",
|
|
57
|
-
"vitest": "^5.0.
|
|
57
|
+
"vitest": "^5.0.5"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=20"
|