@enodo/butterfly-tracker 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +76 -2
- package/dist/butterfly-tracker.min.js +2 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enodo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# butterfly-tracker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cookieless first-party audience tracker for **Enodo Butterfly** sites. One tiny
|
|
4
|
+
`POST {tracker}/h` per page view; the Butterfly CMS turns them into realtime stats,
|
|
5
|
+
trending / rising / top posts (`sort=trending` on the public API) and Performances.
|
|
6
|
+
|
|
7
|
+
- No cookie, no localStorage, no identifier, credentials omitted: built for the CNIL
|
|
8
|
+
audience-measurement exemption, so it does **not** wait for consent.
|
|
9
|
+
- `navigator.sendBeacon` (text/plain, no CORS preflight), `fetch` keepalive fallback.
|
|
10
|
+
- Never throws, never blocks, no-op on the server. ~1.3 kB gzipped.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @enodo/butterfly-tracker
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createTracker } from '@enodo/butterfly-tracker';
|
|
22
|
+
|
|
23
|
+
// Tracker URL derived from the API URL: {id}.pubbtf.eno.do -> {id}.tbtf.eno.do,
|
|
24
|
+
// api{x}.example.com -> t{x}.example.com. Or pass `url` explicitly ('off' disables).
|
|
25
|
+
const tracker = createTracker({ apiUrl: 'https://api.example.com', enabled: !dev });
|
|
26
|
+
|
|
27
|
+
tracker.pageview({ 'content.type': 'post', 'content.id': 123 });
|
|
28
|
+
|
|
29
|
+
// Or a raw hit
|
|
30
|
+
tracker.hit({ t: 'category', id: '4' });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`toHit(layer, { typeKeys, postTypes })` adapts site-specific layers: e.g.
|
|
34
|
+
`{ typeKeys: ['page.type', 'content.type'], postTypes: ['post', 'recipe'] }` sends recipe
|
|
35
|
+
pages (whose `content.id` is a Butterfly post) as `post` so they rank in trending.
|
|
36
|
+
|
|
37
|
+
Pagination / infinite scroll (`page.index > 1`) and the same hit twice in a row for the
|
|
38
|
+
same URL are not sent.
|
|
39
|
+
|
|
40
|
+
## Without a bundler
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@enodo/butterfly-tracker/dist/butterfly-tracker.min.js"
|
|
44
|
+
data-url="https://t.example.com" data-type="post" data-id="123"></script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Sends one hit on load (`data-type` defaults to `page`, `data-auto="false"` to skip) and
|
|
48
|
+
exposes `window.ButterflyTracker`.
|
|
49
|
+
|
|
50
|
+
## Without JavaScript
|
|
51
|
+
|
|
52
|
+
Visitors without JavaScript can be counted with a 1×1 pixel, the same hit keys as a
|
|
53
|
+
query string on `GET /h.gif`:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<noscript>
|
|
57
|
+
<img src="https://t.example.com/h.gif?t=post&id=123" alt="" width="1" height="1"
|
|
58
|
+
style="position:absolute">
|
|
59
|
+
</noscript>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`<noscript>` only renders when JavaScript is off, so it never doubles the script's hit.
|
|
63
|
+
It counts the view and the visitor, but not the traffic source: without JavaScript the
|
|
64
|
+
page cannot read `document.referrer`, so no `e` / `r` is sent.
|
|
65
|
+
|
|
66
|
+
## Hit
|
|
67
|
+
|
|
68
|
+
| key | meaning |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `t` | page type: post, page, category, tag, author, home, search, or any site type |
|
|
71
|
+
| `id` | id of the content in the Butterfly API (post, category, term, author) |
|
|
72
|
+
| `tx` | taxonomy id of a term page |
|
|
73
|
+
| `pi` | page index |
|
|
74
|
+
| `q`, `n` | search query, number of results |
|
|
75
|
+
| `p` | property id, only on a shared tracker host |
|
|
76
|
+
| `e` | entry: `1` on the first hit after a page load (set by the tracker) |
|
|
77
|
+
| `r` | referrer hostname of the entry hit, never the full URL (set by the tracker) |
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";(()=>{var I=Object.defineProperty,v=Object.defineProperties;var B=Object.getOwnPropertyDescriptors;var l=Object.getOwnPropertySymbols;var w=Object.prototype.hasOwnProperty,U=Object.prototype.propertyIsEnumerable;var H=(t,e,n)=>e in t?I(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,y=(t,e)=>{for(var n in e||(e={}))w.call(e,n)&&H(t,n,e[n]);if(l)for(var n of l(e))U.call(e,n)&&H(t,n,e[n]);return t},L=(t,e)=>v(t,B(e));var O=(t,e)=>{var n={};for(var r in t)w.call(t,r)&&e.indexOf(r)<0&&(n[r]=t[r]);if(t!=null&&l)for(var r of l(t))e.indexOf(r)<0&&U.call(t,r)&&(n[r]=t[r]);return n};function g(t){if(!t)return null;let e;try{e=new URL(t).hostname.toLowerCase()}catch(a){return null}let n=/^(\d+)\.pubbtf\.eno\.do$/.exec(e);if(n)return`https://${n[1]}.tbtf.eno.do`;let r=/^api([a-z0-9-]*)\.(.+\..+)$/.exec(e);return r?`https://t${r[1]}.${r[2]}`:null}function q(t,e){return t==="off"||t==="false"?null:t?t.replace(/\/+$/,""):g(e)}function m(t,e={}){var d,c;let n=(d=e.typeKeys)!=null?d:["content.type"],r=(c=e.postTypes)!=null?c:["post"],a=n.map(p=>t[p]).find(p=>typeof p=="string"&&p);if(typeof a!="string")return null;let o=t["content.id"],s=typeof o=="string"&&o||typeof o=="number"?String(o):void 0,u={t:r.includes(a)&&s!==void 0&&s!=="0"?"post":a};return s!==void 0&&(u.id=s),typeof t["content.taxonomy"]=="string"&&(u.tx=t["content.taxonomy"]),typeof t["page.index"]=="number"&&(u.pi=t["page.index"]),typeof t["page.query"]=="string"&&(u.q=t["page.query"]),typeof t["page.results"]=="number"&&(u.n=t["page.results"]),u}function A(t){let e=t!=null?t:typeof document=="undefined"?"":String(document.referrer||"");if(e)try{return new URL(e).hostname.toLowerCase()||void 0}catch(n){return}}function F(t,e){let n=typeof navigator=="undefined"?void 0:navigator;if(typeof(n==null?void 0:n.sendBeacon)=="function")try{if(n.sendBeacon(t,new Blob([e],{type:"text/plain"})))return!0}catch(r){}return typeof fetch=="function"?(fetch(t,{method:"POST",body:e,keepalive:!0,credentials:"omit",headers:{"Content-Type":"text/plain"}}).catch(()=>{}),!0):!1}function h(t={}){let e=t.enabled===!1?null:q(t.url,t.apiUrl),n,r=!1;function a(o){var s;try{if(!e||typeof window=="undefined"||((s=o.pi)!=null?s:1)>1)return!1;let f=o,{e:u,r:d}=f,c=O(f,["e","r"]),p=t.propertyId?L(y({},c),{p:t.propertyId}):y({},c),T=`${window.location.href}
|
|
2
|
+
${JSON.stringify(p)}`;if(T===n)return!1;if(n=T,!r){p.e=1;let b=A();b&&(p.r=b)}let k=F(`${e}/h`,JSON.stringify(p));return k&&(r=!0),k}catch(u){return!1}}return{url:e,hit:a,pageview(o,s){let f=m(o,s);return f?a(f):!1}}}var x=document.currentScript,$,i=($=x==null?void 0:x.dataset)!=null?$:{},S=h({url:i.url,apiUrl:i.apiUrl,propertyId:i.property?parseInt(i.property,10):void 0});window.ButterflyTracker={tracker:S,createTracker:h,toHit:m,trackerUrlFromApiUrl:g};if(i.auto!=="false"){let t={t:i.type||"page"};i.id&&(t.id=i.id),i.taxonomy&&(t.tx=i.taxonomy),S.hit(t)}})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @enodo/butterfly-tracker — cookieless first-party audience tracker for Butterfly sites.
|
|
3
|
+
*
|
|
4
|
+
* Sends one small `POST {trackerUrl}/h` per page view (text/plain JSON via
|
|
5
|
+
* `navigator.sendBeacon`, `fetch` keepalive fallback). No cookie, no storage, no
|
|
6
|
+
* identifier, credentials omitted: designed for the CNIL audience-measurement
|
|
7
|
+
* exemption, so it does not wait for consent. Never throws, never blocks.
|
|
8
|
+
*/
|
|
9
|
+
/** Body of a hit. Keys are short on purpose. */
|
|
10
|
+
export type Hit = {
|
|
11
|
+
/** Page type: post | page | category | tag | author | home | search | any site-specific type */
|
|
12
|
+
t: string;
|
|
13
|
+
/** Id of the content in the Butterfly API (post, category, term, author) */
|
|
14
|
+
id?: string;
|
|
15
|
+
/** Taxonomy id of a term page */
|
|
16
|
+
tx?: string;
|
|
17
|
+
/** Page index: > 1 (pagination, infinite scroll) is not a view and is not sent */
|
|
18
|
+
pi?: number;
|
|
19
|
+
/** Search query */
|
|
20
|
+
q?: string;
|
|
21
|
+
/** Number of search results (0 marks a zero-result search) */
|
|
22
|
+
n?: number;
|
|
23
|
+
/** Property id, only needed on a shared tracker host */
|
|
24
|
+
p?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Entry: set on the first hit after a full page load (a visit landing on the
|
|
27
|
+
* site, or a full navigation). Set by the tracker, not by callers.
|
|
28
|
+
*/
|
|
29
|
+
e?: 1;
|
|
30
|
+
/**
|
|
31
|
+
* Referrer HOSTNAME of the entry hit (never the full URL). Absent for a direct
|
|
32
|
+
* visit. The tracker maps it to a source (Google, Facebook...) and drops it
|
|
33
|
+
* when it is the site itself.
|
|
34
|
+
*/
|
|
35
|
+
r?: string;
|
|
36
|
+
};
|
|
37
|
+
export type Layer = Record<string, unknown>;
|
|
38
|
+
export type ToHitOptions = {
|
|
39
|
+
/** Layer keys read for the page type, first match wins. Default `['content.type']`. */
|
|
40
|
+
typeKeys?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Page types whose `content.id` is a Butterfly post id, sent as `post` so they
|
|
43
|
+
* rank in trending (e.g. `['post', 'recipe']`). Default `['post']`.
|
|
44
|
+
*/
|
|
45
|
+
postTypes?: string[];
|
|
46
|
+
};
|
|
47
|
+
export type TrackerOptions = {
|
|
48
|
+
/** Tracker base URL. `off` / `false` disables. When empty, derived from `apiUrl`. */
|
|
49
|
+
url?: string | null;
|
|
50
|
+
/** Butterfly public API URL, used to derive the tracker URL. */
|
|
51
|
+
apiUrl?: string | null;
|
|
52
|
+
/** Property id, only for a shared tracker host. */
|
|
53
|
+
propertyId?: number;
|
|
54
|
+
/** Master switch (e.g. `!dev`). Default true. */
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
};
|
|
57
|
+
export type Tracker = {
|
|
58
|
+
/** Resolved tracker base URL, `null` when disabled. */
|
|
59
|
+
readonly url: string | null;
|
|
60
|
+
/** Sends a hit. Returns true when it was handed to the browser. */
|
|
61
|
+
hit(hit: Hit): boolean;
|
|
62
|
+
/** Maps a data layer (`content.type`, `content.id`…) to a hit and sends it. */
|
|
63
|
+
pageview(layer: Layer, options?: ToHitOptions): boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Tracker URL derived from the Butterfly API URL, following the domain convention:
|
|
67
|
+
* https://{id}.pubbtf.eno.do -> https://{id}.tbtf.eno.do
|
|
68
|
+
* https://api{x}.example.com -> https://t{x}.example.com (api. -> t., apibe. -> tbe.)
|
|
69
|
+
* `null` when the API URL follows neither pattern.
|
|
70
|
+
*/
|
|
71
|
+
export declare function trackerUrlFromApiUrl(apiUrl: string | null | undefined): string | null;
|
|
72
|
+
/** Explicit URL when set (`off` / `false` disables), otherwise derived from the API URL. */
|
|
73
|
+
export declare function resolveTrackerUrl(url: string | null | undefined, apiUrl?: string | null): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Maps a data layer to a hit, `null` when there is nothing to count.
|
|
76
|
+
* Reads `content.type` (or `typeKeys`), `content.id`, `content.taxonomy`,
|
|
77
|
+
* `page.index`, `page.query` and `page.results`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function toHit(layer: Layer, options?: ToHitOptions): Hit | null;
|
|
80
|
+
/** Hostname of `document.referrer`, lowercase, `undefined` when empty or unparseable. */
|
|
81
|
+
export declare function referrerHost(referrer?: string): string | undefined;
|
|
82
|
+
export declare function createTracker(options?: TrackerOptions): Tracker;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @enodo/butterfly-tracker — cookieless first-party audience tracker for Butterfly sites.
|
|
3
|
+
*
|
|
4
|
+
* Sends one small `POST {trackerUrl}/h` per page view (text/plain JSON via
|
|
5
|
+
* `navigator.sendBeacon`, `fetch` keepalive fallback). No cookie, no storage, no
|
|
6
|
+
* identifier, credentials omitted: designed for the CNIL audience-measurement
|
|
7
|
+
* exemption, so it does not wait for consent. Never throws, never blocks.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Tracker URL derived from the Butterfly API URL, following the domain convention:
|
|
11
|
+
* https://{id}.pubbtf.eno.do -> https://{id}.tbtf.eno.do
|
|
12
|
+
* https://api{x}.example.com -> https://t{x}.example.com (api. -> t., apibe. -> tbe.)
|
|
13
|
+
* `null` when the API URL follows neither pattern.
|
|
14
|
+
*/
|
|
15
|
+
export function trackerUrlFromApiUrl(apiUrl) {
|
|
16
|
+
if (!apiUrl) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
let host;
|
|
20
|
+
try {
|
|
21
|
+
host = new URL(apiUrl).hostname.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const own = /^(\d+)\.pubbtf\.eno\.do$/.exec(host);
|
|
27
|
+
if (own) {
|
|
28
|
+
return `https://${own[1]}.tbtf.eno.do`;
|
|
29
|
+
}
|
|
30
|
+
const custom = /^api([a-z0-9-]*)\.(.+\..+)$/.exec(host);
|
|
31
|
+
if (custom) {
|
|
32
|
+
return `https://t${custom[1]}.${custom[2]}`;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Explicit URL when set (`off` / `false` disables), otherwise derived from the API URL. */
|
|
37
|
+
export function resolveTrackerUrl(url, apiUrl) {
|
|
38
|
+
if (url === 'off' || url === 'false') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (url) {
|
|
42
|
+
return url.replace(/\/+$/, '');
|
|
43
|
+
}
|
|
44
|
+
return trackerUrlFromApiUrl(apiUrl);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Maps a data layer to a hit, `null` when there is nothing to count.
|
|
48
|
+
* Reads `content.type` (or `typeKeys`), `content.id`, `content.taxonomy`,
|
|
49
|
+
* `page.index`, `page.query` and `page.results`.
|
|
50
|
+
*/
|
|
51
|
+
export function toHit(layer, options = {}) {
|
|
52
|
+
const typeKeys = options.typeKeys ?? ['content.type'];
|
|
53
|
+
const postTypes = options.postTypes ?? ['post'];
|
|
54
|
+
const type = typeKeys.map((k) => layer[k]).find((v) => typeof v === 'string' && v);
|
|
55
|
+
if (typeof type !== 'string') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const rawId = layer['content.id'];
|
|
59
|
+
const id = (typeof rawId === 'string' && rawId) || typeof rawId === 'number' ? String(rawId) : undefined;
|
|
60
|
+
const isPost = postTypes.includes(type) && id !== undefined && id !== '0';
|
|
61
|
+
const hit = { t: isPost ? 'post' : type };
|
|
62
|
+
if (id !== undefined) {
|
|
63
|
+
hit.id = id;
|
|
64
|
+
}
|
|
65
|
+
if (typeof layer['content.taxonomy'] === 'string') {
|
|
66
|
+
hit.tx = layer['content.taxonomy'];
|
|
67
|
+
}
|
|
68
|
+
if (typeof layer['page.index'] === 'number') {
|
|
69
|
+
hit.pi = layer['page.index'];
|
|
70
|
+
}
|
|
71
|
+
if (typeof layer['page.query'] === 'string') {
|
|
72
|
+
hit.q = layer['page.query'];
|
|
73
|
+
}
|
|
74
|
+
if (typeof layer['page.results'] === 'number') {
|
|
75
|
+
hit.n = layer['page.results'];
|
|
76
|
+
}
|
|
77
|
+
return hit;
|
|
78
|
+
}
|
|
79
|
+
/** Hostname of `document.referrer`, lowercase, `undefined` when empty or unparseable. */
|
|
80
|
+
export function referrerHost(referrer) {
|
|
81
|
+
const value = referrer ?? (typeof document === 'undefined' ? '' : String(document.referrer || ''));
|
|
82
|
+
if (!value) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return new URL(value).hostname.toLowerCase() || undefined;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function send(url, body) {
|
|
93
|
+
const nav = typeof navigator === 'undefined' ? undefined : navigator;
|
|
94
|
+
if (typeof nav?.sendBeacon === 'function') {
|
|
95
|
+
try {
|
|
96
|
+
if (nav.sendBeacon(url, new Blob([body], { type: 'text/plain' }))) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// fall through to fetch
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (typeof fetch === 'function') {
|
|
105
|
+
fetch(url, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body,
|
|
108
|
+
keepalive: true,
|
|
109
|
+
credentials: 'omit',
|
|
110
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
111
|
+
}).catch(() => undefined);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
export function createTracker(options = {}) {
|
|
117
|
+
const url = options.enabled === false ? null : resolveTrackerUrl(options.url, options.apiUrl);
|
|
118
|
+
let last;
|
|
119
|
+
// The first hit sent after the page load is the entry (one per tracker instance)
|
|
120
|
+
let entrySent = false;
|
|
121
|
+
function hit(input) {
|
|
122
|
+
try {
|
|
123
|
+
if (!url || typeof window === 'undefined') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Pagination / infinite scroll are not new views
|
|
127
|
+
if ((input.pi ?? 1) > 1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const { e: _e, r: _r, ...hitInput } = input;
|
|
131
|
+
const payload = options.propertyId
|
|
132
|
+
? { ...hitInput, p: options.propertyId }
|
|
133
|
+
: { ...hitInput };
|
|
134
|
+
// Same hit for the same URL twice in a row (effect re-run, data reload): once
|
|
135
|
+
const key = `${window.location.href}\n${JSON.stringify(payload)}`;
|
|
136
|
+
if (key === last) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
last = key;
|
|
140
|
+
if (!entrySent) {
|
|
141
|
+
payload.e = 1;
|
|
142
|
+
const r = referrerHost();
|
|
143
|
+
if (r) {
|
|
144
|
+
payload.r = r;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const sent = send(`${url}/h`, JSON.stringify(payload));
|
|
148
|
+
if (sent) {
|
|
149
|
+
entrySent = true;
|
|
150
|
+
}
|
|
151
|
+
return sent;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false; // analytics must never break the page
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
url,
|
|
159
|
+
hit,
|
|
160
|
+
pageview(layer, toHitOptions) {
|
|
161
|
+
const h = toHit(layer, toHitOptions);
|
|
162
|
+
return h ? hit(h) : false;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enodo/butterfly-tracker",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cookieless first-party audience tracker for Enodo Butterfly sites (pageview hits, no cookie, no storage)",
|
|
6
|
+
"author": "Jerome Musialak <jerome@eno.do>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/enodo-io/butterfly-tracker",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/enodo-io/butterfly-tracker/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/enodo-io/butterfly-tracker.git"
|
|
15
|
+
},
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./script": "./dist/butterfly-tracker.min.js"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc --project tsconfig.build.json && esbuild src/script.ts --bundle --minify --format=iife --target=es2017 --outfile=dist/butterfly-tracker.min.js",
|
|
28
|
+
"lint": "eslint --config eslint.config.mjs --max-warnings=0 .",
|
|
29
|
+
"lint:tsc": "tsc --noEmit",
|
|
30
|
+
"format": "prettier --write .",
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"prepublishOnly": "npm run lint && npm run lint:tsc && npm test && npm run build"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"butterfly",
|
|
36
|
+
"analytics",
|
|
37
|
+
"tracker",
|
|
38
|
+
"cookieless",
|
|
39
|
+
"pageview"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^9.39.5",
|
|
43
|
+
"@types/node": "^24.19.2",
|
|
44
|
+
"esbuild": "^0.25.12",
|
|
45
|
+
"eslint": "^9.39.5",
|
|
46
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
48
|
+
"eslint-plugin-import-x": "^4.18.0",
|
|
49
|
+
"eslint-plugin-prettier": "^5.5.7",
|
|
50
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
51
|
+
"prettier": "^3.9.10",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"typescript-eslint": "^8.72.1",
|
|
54
|
+
"vitest": "^3.2.7"
|
|
55
|
+
}
|
|
56
|
+
}
|