@backendfree/webanalytics 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # @backendfree/webanalytics
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Report page views to BackendFree from your own server, for the visitors and
4
+ crawlers a browser snippet never sees.
5
+
6
+ The snippet on the Tracking screen covers people with JavaScript. This covers
7
+ everything else: crawlers, which mostly do not run scripts at all, and any
8
+ request you want measured before a page is rendered.
9
+
10
+ ```sh
11
+ npm install @backendfree/webanalytics
12
+ ```
13
+
14
+ ## Using it
15
+
16
+ In a Next.js middleware:
17
+
18
+ ```ts
19
+ import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
20
+ import { Reporter } from '@backendfree/webanalytics';
21
+
22
+ const reporter = new Reporter({
23
+ origin: 'https://backendfree.com',
24
+ key: process.env.BACKENDFREE_SECRET_KEY!,
25
+ });
26
+
27
+ export function middleware(request: NextRequest, event: NextFetchEvent) {
28
+ reporter.pageview({
29
+ url: request.url,
30
+ referrer: request.headers.get('referer') ?? '',
31
+ ip: request.headers.get('cf-connecting-ip') ?? '',
32
+ userAgent: request.headers.get('user-agent') ?? '',
33
+ optedOut: request.headers.get('sec-gpc') === '1' || request.headers.get('dnt') === '1',
34
+ });
35
+ event.waitUntil(reporter.flush());
36
+ return NextResponse.next();
37
+ }
38
+ ```
39
+
40
+ `pageview` and `event` return immediately. Nothing is awaited, nothing throws,
41
+ and a collector that is down or slow cannot delay or break the page.
42
+
43
+ The middleware hands `flush()` to `waitUntil`, which keeps the function alive
44
+ long enough for the report to leave without the page waiting for it. On any
45
+ other serverless platform, do the same with whatever it calls that:
46
+
47
+ ```ts
48
+ context.waitUntil(reporter.flush());
49
+ ```
50
+
51
+ ## A secret key, and it refuses a publishable one
52
+
53
+ Every report asserts who the visitor was, because your server is the only thing
54
+ that saw them. A publishable key is public by design: it sits in the page source
55
+ of every site using it. If this endpoint accepted one, anybody who read it could
56
+ forge visitors and spend your events allowance while looking like real traffic.
57
+
58
+ So this needs `sk_`, the platform refuses anything else with a 401, and the
59
+ constructor refuses it earlier so the stack trace points at the line that is
60
+ wrong.
61
+
62
+ Keep the key on the server. It is the same key that can read your payments.
63
+
64
+ ## What we do with what you send
65
+
66
+ The address and the User-Agent go into a hash with a salt that rotates at your
67
+ project's local midnight and is never written down, and neither is ever stored.
68
+ Same as what the browser snippet does with the address it arrives from, which is
69
+ why sites using either need no cookie banner. The full list is on the Tracking
70
+ screen.
71
+
72
+ Send the **visitor's** address, not your server's: read it from whatever header
73
+ your proxy sets (`cf-connecting-ip`, `x-real-ip`, or the entry your own proxy
74
+ appended to `x-forwarded-for`, at its right-hand end, since the left is whatever
75
+ the visitor sent). Your own address for everybody makes the whole site look
76
+ like one person. A report with no address still records; it just counts fewer
77
+ visitors than there were.
78
+
79
+ Send the **visitor's** User-Agent too. It is what tells a crawler from a person,
80
+ and without it a crawler is counted as a visitor and spends an event.
81
+
82
+ Pass the **visitor's** opt-out. Somebody whose browser sends `Sec-GPC: 1` or
83
+ `DNT: 1` is not measured by the snippet, and only your server saw those headers,
84
+ so `optedOut` is how the same rule reaches this door. An opted-out hit is never
85
+ sent. Writing your own client instead, send `"opted_out": true` on the event and
86
+ the platform drops it before anything is derived from the address.
87
+
88
+ ## Options
89
+
90
+ | option | default | |
91
+ |---|---|---|
92
+ | `origin` | required | Where the platform runs, no trailing path |
93
+ | `key` | required | `sk_live_...` |
94
+ | `batchSize` | `20` | Hits gathered before sending. Capped at 100 |
95
+ | `interval` | `1000` | How long a partly full batch waits, in ms |
96
+ | `timeout` | `10000` | Per-request, in ms. `0` disables it |
97
+ | `fetch` | global | Your own: a test double, a Worker's fetcher |
98
+ | `onError` | silence | The only place a failed report is visible |
99
+
100
+ `onError` defaults to doing nothing on purpose. During an outage the alternative
101
+ is a log line per page view, which fills your log budget with our problem.
102
+
103
+ ## What it does not do
104
+
105
+ **It does not read anything.** There is no method here that returns figures: the
106
+ Traffic screen in the dashboard is where those are read, and a key that could
107
+ report and read would be a wider key than reporting needs.
108
+
109
+ **It does not retry.** A page view that failed to send is a page view lost, and
110
+ that is the right trade: a queue that grows during an outage is memory on
111
+ somebody's web server, and the figure it protects is a count.
112
+
113
+ **It does not trim your URLs.** The query string is dropped and `utm_*` kept on
114
+ arrival, in the one place that rule lives. Doing it here as well would be a
115
+ second implementation of it, and the day they disagreed your campaign tags would
116
+ go missing with nothing failing.
@@ -0,0 +1 @@
1
+ {"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.es2021.d.ts","../../../node_modules/typescript/lib/lib.es2022.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../core/dist/types.d.ts","../../core/dist/client.d.ts","../../core/dist/errors.d.ts","../../core/dist/webhooks.d.ts","../../core/dist/index.d.ts","../src/types.ts","../src/index.ts","../../../node_modules/@types/node/compatibility/iterators.d.ts","../../../node_modules/@types/node/globals.typedarray.d.ts","../../../node_modules/@types/node/buffer.buffer.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/web-globals/abortcontroller.d.ts","../../../node_modules/@types/node/web-globals/crypto.d.ts","../../../node_modules/@types/node/web-globals/domexception.d.ts","../../../node_modules/@types/node/web-globals/events.d.ts","../../../node_modules/undici-types/utility.d.ts","../../../node_modules/undici-types/header.d.ts","../../../node_modules/undici-types/readable.d.ts","../../../node_modules/undici-types/fetch.d.ts","../../../node_modules/undici-types/formdata.d.ts","../../../node_modules/undici-types/connector.d.ts","../../../node_modules/undici-types/client-stats.d.ts","../../../node_modules/undici-types/client.d.ts","../../../node_modules/undici-types/errors.d.ts","../../../node_modules/undici-types/dispatcher.d.ts","../../../node_modules/undici-types/global-dispatcher.d.ts","../../../node_modules/undici-types/global-origin.d.ts","../../../node_modules/undici-types/pool-stats.d.ts","../../../node_modules/undici-types/pool.d.ts","../../../node_modules/undici-types/handlers.d.ts","../../../node_modules/undici-types/balanced-pool.d.ts","../../../node_modules/undici-types/round-robin-pool.d.ts","../../../node_modules/undici-types/h2c-client.d.ts","../../../node_modules/undici-types/agent.d.ts","../../../node_modules/undici-types/mock-interceptor.d.ts","../../../node_modules/undici-types/mock-call-history.d.ts","../../../node_modules/undici-types/mock-agent.d.ts","../../../node_modules/undici-types/mock-client.d.ts","../../../node_modules/undici-types/mock-pool.d.ts","../../../node_modules/undici-types/snapshot-agent.d.ts","../../../node_modules/undici-types/mock-errors.d.ts","../../../node_modules/undici-types/proxy-agent.d.ts","../../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../../node_modules/undici-types/retry-handler.d.ts","../../../node_modules/undici-types/retry-agent.d.ts","../../../node_modules/undici-types/api.d.ts","../../../node_modules/undici-types/cache-interceptor.d.ts","../../../node_modules/undici-types/interceptors.d.ts","../../../node_modules/undici-types/util.d.ts","../../../node_modules/undici-types/cookies.d.ts","../../../node_modules/undici-types/patch.d.ts","../../../node_modules/undici-types/websocket.d.ts","../../../node_modules/undici-types/eventsource.d.ts","../../../node_modules/undici-types/diagnostics-channel.d.ts","../../../node_modules/undici-types/content-type.d.ts","../../../node_modules/undici-types/cache.d.ts","../../../node_modules/undici-types/index.d.ts","../../../node_modules/@types/node/web-globals/fetch.d.ts","../../../node_modules/@types/node/web-globals/navigator.d.ts","../../../node_modules/@types/node/web-globals/storage.d.ts","../../../node_modules/@types/node/web-globals/streams.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/inspector.generated.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/readline/promises.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/sea.d.ts","../../../node_modules/@types/node/sqlite.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/index.d.ts","../../../../../../../../node_modules/@types/use-sync-external-store/index.d.ts"],"fileIdsList":[[70,122,123,125,142,143],[70,124,125,142,143],[125,142,143],[70,125,130,142,143,160],[70,125,126,131,136,142,143,145,157,168],[70,125,126,127,136,142,143,145],[70,125,142,143],[70,125,128,142,143,169],[70,125,129,130,137,142,143,146],[70,125,130,142,143,157,165],[70,125,131,133,136,142,143,145],[70,124,125,132,142,143],[70,125,133,134,142,143],[70,125,135,136,142,143],[70,124,125,136,142,143],[70,125,136,137,138,142,143,157,168],[70,125,136,137,138,142,143,152,157,160],[70,117,125,133,136,139,142,143,145,157,168],[70,125,136,137,139,140,142,143,145,157,165,168],[70,125,139,141,142,143,157,165,168],[68,69,70,71,72,73,74,75,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174],[70,125,136,142,143],[70,125,142,143,144,168],[70,125,133,136,142,143,145,157],[70,125,142,143,146],[70,125,142,143,147],[70,124,125,142,143,148],[70,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174],[70,125,142,143,150],[70,125,142,143,151],[70,125,136,142,143,152,153],[70,125,142,143,152,154,169,171],[70,125,137,142,143],[70,125,136,142,143,157,158,160],[70,125,142,143,159,160],[70,125,142,143,157,158],[70,125,142,143,160],[70,125,142,143,161],[70,122,125,142,143,157,162,168],[70,125,136,142,143,163,164],[70,125,142,143,163,164],[70,125,130,142,143,145,157,165],[70,125,142,143,166],[70,125,142,143,145,167],[70,125,139,142,143,151,168],[70,125,130,142,143,169],[70,125,142,143,157,170],[70,125,142,143,144,171],[70,125,142,143,172],[70,125,130,142,143],[70,117,125,142,143],[70,125,142,143,173],[70,117,125,136,138,142,143,148,157,160,168,170,171,173],[70,125,142,143,157,174],[70,82,85,88,89,125,142,143,168],[70,85,125,142,143,157,168],[70,85,89,125,142,143,168],[70,125,142,143,157],[70,79,125,142,143],[70,83,125,142,143],[70,81,82,85,125,142,143,168],[70,125,142,143,145,165],[70,125,142,143,175],[70,79,125,142,143,175],[70,81,85,125,142,143,145,168],[70,76,77,78,80,84,125,136,142,143,157,168],[70,85,94,102,125,142,143],[70,77,83,125,142,143],[70,85,111,112,125,142,143],[70,77,80,85,125,142,143,160,168,175],[70,85,125,142,143],[70,81,85,125,142,143,168],[70,76,125,142,143],[70,79,80,81,83,84,85,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,112,113,114,115,116,125,142,143],[70,85,104,107,125,133,142,143],[70,85,94,95,96,125,142,143],[70,83,85,95,97,125,142,143],[70,84,125,142,143],[70,77,79,85,125,142,143],[70,85,89,95,97,125,142,143],[70,89,125,142,143],[70,83,85,88,125,142,143,168],[70,77,81,85,94,125,142,143],[70,85,104,125,142,143],[70,97,125,142,143],[70,79,85,111,125,142,143,160,173,175],[61,70,125,142,143],[61,62,63,64,70,125,142,143],[63,70,125,142,143],[65,66,70,125,142,143]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fd2a0fd0d723bc5f0c7a45564bccf9c19f17f244bebdfda73a954fceb747d95","impliedFormat":99},{"version":"90322bec0644e23332de310a1dc3b078dee1955bb299d15d61381c79393ebeaf","impliedFormat":99},{"version":"f38ea2796a1641ceff6b6c660273031fbfe3f194edcde5d43e0cd7425394fe5d","impliedFormat":99},{"version":"1dceb3d3ac3986d06a1a5d6fad256a42556b1d9269be019cf29e618016ce8618","impliedFormat":99},{"version":"eceb8dc64b11def752a0766ed854b04fa747a61a2bea686f1823e6da8db0e63a","impliedFormat":99},{"version":"55f6ac520ce7e62d46e226d9034575ba5763a5326b39cad3bae85f2917f2b60b","signature":"7a135ab22b79f1a5499f132e9f7fcced421ff120a0258a680af068d92a864bfd","impliedFormat":99},{"version":"0a44f84fdbfd32937b22d1908762733a18d7e6508513712fb9e8502a4f42455c","signature":"b0feeb3011232ef7f59f46f46bf0ad44c9cd7d8a0abe1a111059f3f13741861c","impliedFormat":99},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"378281aa35786c27d5811af7e6bcaa492eebd0c7013d48137c35bbc69a2b9751","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"1b2dd1cbeb0cc6ae20795958ba5950395ebb2849b7c8326853dd15530c77ab0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"387a023d363f755eb63450a66c28b14cdd7bc30a104565e2dbf0a8988bb4a56c","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"074de5b2fdead0165a2757e3aaef20f27a6347b1c36adea27d51456795b37682","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"24371e69a38fc33e268d4a8716dbcda430d6c2c414a99ff9669239c4b8f40dea","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"3e11fce78ad8c0e1d1db4ba5f0652285509be3acdd519529bc8fcef85f7dafd9","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"c63b9ada8c72f95aac5db92aea07e5e87ec810353cdf63b2d78f49a58662cf6c","impliedFormat":1},{"version":"1cc2a09e1a61a5222d4174ab358a9f9de5e906afe79dbf7363d871a7edda3955","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"b64d4d1c5f877f9c666e98e833f0205edb9384acc46e98a1fef344f64d6aba44","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"c9381908473a1c92cb8c516b184e75f4d226dad95c3a85a5af35f670064d9a2f","impliedFormat":1},{"version":"f724236417941ea77ec8d38c6b7021f5fb7f8521c7f8c1538e87661f2c6a0774","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d97fb21da858fb18b8ae72c314e9743fd52f73ebe2764e12af1db32fc03f853f","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ea15fd99b2e34cb25fe8346c955000bb70c8b423ae4377a972ef46bfb37f595","impliedFormat":1},{"version":"7cf69dd5502c41644c9e5106210b5da7144800670cbe861f66726fa209e231c4","impliedFormat":1},{"version":"72c1f5e0a28e473026074817561d1bc9647909cf253c8d56c41d1df8d95b85f7","impliedFormat":1},{"version":"f9b4137a0d285bd77dba2e6e895530112264310ae47e07bf311feae428fb8b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"c06b2652ffeb89afd0f1c52c165ced77032f9cd09bc481153fbd6b5504c69494","impliedFormat":1},{"version":"51aecd2df90a3cffea1eb4696b33b2d78594ea2aa2138e6b9471ec4841c6c2ee","impliedFormat":1},{"version":"9d8f9e63e29a3396285620908e7f14d874d066caea747dc4b2c378f0599166b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"612422d5ba6b4a5c4537f423e9199645468ad80a689801da63ab7edb43f7b835","impliedFormat":1},{"version":"db9ada976f9e52e13f7ae8b9a320f4b67b87685938c5879187d8864b2fbe97f3","impliedFormat":1},{"version":"9f39e70a354d0fba29ac3cdf6eca00b7f9e96f64b2b2780c432e8ea27f133743","impliedFormat":1},{"version":"0dace96cc0f7bc6d0ee2044921bdf19fe42d16284dbcc8ae200800d1c9579335","impliedFormat":1},{"version":"a2e2bbde231b65c53c764c12313897ffdfb6c49183dd31823ee2405f2f7b5378","impliedFormat":1},{"version":"1cbdcdef62bf42688e5d825898c3a59458466c48ad8a4ef3f88d8a1cd04310d6","impliedFormat":1},{"version":"c64e1888baaa3253ca4405b455e4bf44f76357868a1bd0a52998ade9a092ad78","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc8c6f5322961b56d9906601b20798725df60baeab45ec014fba9f795d5596fd","impliedFormat":1},{"version":"67e435fa530778903f5525904e6645b7368eb5ac622055febd19bb399e328ccd","impliedFormat":1},{"version":"060d305fe4494d8cb2b99d620928d369d1ee55c1645f5e729a2aca07d0f108cb","impliedFormat":1},{"version":"5b22bf96be06fca1e134e3a85351ffe7aa2edf0c9a5bd175b7239ee48186dbf5","impliedFormat":1},{"version":"0c50296ee73dae94efc3f0da4936b1146ca6ce2217acfabb44c19c9a33fa30e5","impliedFormat":1},{"version":"bbf42f98a5819f4f06e18c8b669a994afe9a17fe520ae3454a195e6eabf7700d","impliedFormat":1},{"version":"5b48cc670da1dfe926036deaf349085caf822def460a6d50de23f1cc12a5f8cd","impliedFormat":1},{"version":"418f34087d52d001e2688a6282a5de9bf583ff771ed5546af0ae36e4f60a458b","affectsGlobalScope":true,"impliedFormat":1},{"version":"036c9a03b01281f99f8251fdc7088e894566be44b3f139a9d48501a978b611df","impliedFormat":1},{"version":"ff65b8a8bd380c6d129becc35de02f7c29ad7ce03300331ca91311fb4044d1a9","impliedFormat":1},{"version":"04bf1aa481d1adfb16d93d76e44ce71c51c8ef68039d849926551199489637f6","impliedFormat":1},{"version":"2c9adcc85574b002c9a6311ff2141055769e0071856ec979d92ff989042b1f1b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1569ad5969ae0de6ad0bdfaaab0dabcd8878bc3147ab096206eed2019bb84e7f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a58a15da4c5ba3df60c910a043281256fa52d36a0fcdef9b9100c646282e88dd","impliedFormat":1},{"version":"b36beffbf8acdc3ebc58c8bb4b75574b31a2169869c70fc03f82895b93950a12","impliedFormat":1},{"version":"42a350ce4f6f291198dc89833afcc4fd078248b4e1fdc9d05c846c66a670f710","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"8c81fd4a110490c43d7c578e8c6f69b3af01717189196899a6a44f93daa57a3a","impliedFormat":1},{"version":"5fb39858b2459864b139950a09adae4f38dad87c25bf572ce414f10e4bd7baab","impliedFormat":1},{"version":"b03754211d839cdc48979996525a32eb62130682c2f9801dfc2f25533ce7238d","impliedFormat":1},{"version":"3910dab597c40e173bf0e0d419d3ce9682c54ebf6ae84849f9b829b1451a17ec","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"486c074a5c0f2254345c0d1c9540380f5463999e42d7e1a159305ea823d3c4b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"c119835edf36415081dfd9ed15fc0cd37aaa28d232be029ad073f15f3d88c323","impliedFormat":1},{"version":"f4d99ac08c4097a3cc202bec2c7198c19dcdd80d4ec6c62d1356b10d03b119d5","impliedFormat":1},{"version":"9705cd157ffbb91c5cab48bdd2de5a437a372e63f870f8a8472e72ff634d47c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ae86f30d5d10e4f75ce8dcb6e1bd3a12ecec3d071a21e8f462c5c85c678efb41","impliedFormat":1},{"version":"ca77e83162eccec633513723d178d4203bdd5465297267696be08013bab0aa1d","impliedFormat":1},{"version":"e03460fe72b259f6d25ad029f085e4bedc3f90477da4401d8fbc1efa9793230e","impliedFormat":1},{"version":"4286a3a6619514fca656089aee160bb6f2e77f4dd53dc5a96b26a0b4fc778055","impliedFormat":1},{"version":"f61c153d5abbe4381a1fba0d41be0babdd5de51c626ee7ff14cede14dedc33f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7803171d745ab18e77f16bf82e742268ff72b7f7382ab191e4277527580c2d89","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e9ba65cd3d6d194bdda7343f9f2917f7144ead96a924ca58d1a14919992dc20","impliedFormat":1},{"version":"255d948f87f24ffd57bcb2fdf95792fd418a2e1f712a98cf2cce88744d75085c","impliedFormat":1},{"version":"0d5b085f36e6dc55bc6332ecb9c733be3a534958c238fb8d8d18d4a2b6f2a15a","impliedFormat":1},{"version":"0b0dfb2d3e8420ead06f6a11acfb02ddcec42257e6ce391993e895ba0af86de4","affectsGlobalScope":true,"impliedFormat":1},{"version":"bfd3b3c21a56104693183942e221c1896ee23bcb8f8d91ab0b941f7b32985411","impliedFormat":1},{"version":"d7e9ab1b0996639047c61c1e62f85c620e4382206b3abb430d9a21fb7bc23c77","impliedFormat":1},{"version":"61f41da9aaa809e5142b1d849d4e70f3e09913a5cb32c629bf6e61ef27967ff7","impliedFormat":1}],"root":[66,67],"options":{"composite":true,"declaration":true,"declarationMap":true,"module":199,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noUncheckedIndexedAccess":true,"outDir":"./","rootDir":"../src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9,"tsBuildInfoFile":"./.tsbuildinfo","verbatimModuleSyntax":true},"referencedMap":[[122,1],[123,1],[124,2],[70,3],[125,4],[126,5],[127,6],[68,7],[128,8],[129,9],[130,10],[131,11],[132,12],[133,13],[134,13],[135,14],[136,15],[137,16],[138,17],[71,7],[69,7],[139,18],[140,19],[141,20],[175,21],[142,22],[143,7],[144,23],[145,24],[146,25],[147,26],[148,27],[149,28],[150,29],[151,30],[152,31],[153,31],[154,32],[155,7],[156,33],[157,34],[159,35],[158,36],[160,37],[161,38],[162,39],[163,40],[164,41],[165,42],[166,43],[167,44],[168,45],[169,46],[170,47],[171,48],[172,49],[72,7],[73,50],[74,7],[75,7],[118,51],[119,52],[120,7],[121,37],[173,53],[174,54],[59,7],[60,7],[10,7],[12,7],[11,7],[2,7],[13,7],[14,7],[15,7],[16,7],[17,7],[18,7],[19,7],[20,7],[3,7],[21,7],[22,7],[4,7],[23,7],[27,7],[24,7],[25,7],[26,7],[28,7],[29,7],[30,7],[5,7],[31,7],[32,7],[33,7],[34,7],[6,7],[38,7],[35,7],[36,7],[37,7],[39,7],[7,7],[40,7],[45,7],[46,7],[41,7],[42,7],[43,7],[44,7],[8,7],[50,7],[47,7],[48,7],[49,7],[51,7],[9,7],[52,7],[53,7],[54,7],[56,7],[55,7],[1,7],[57,7],[58,7],[94,55],[106,56],[91,57],[107,58],[116,59],[82,60],[83,61],[81,62],[115,63],[110,64],[114,65],[85,66],[103,67],[84,68],[113,69],[79,70],[80,64],[86,71],[87,7],[93,72],[90,71],[77,73],[117,74],[108,75],[97,76],[96,71],[98,77],[101,78],[95,79],[99,80],[111,63],[88,81],[89,82],[102,83],[78,58],[105,84],[104,71],[92,82],[100,85],[109,7],[76,7],[112,86],[62,87],[63,7],[65,88],[61,7],[64,89],[67,90],[66,7],[176,7]],"latestChangedDtsFile":"./index.d.ts","version":"5.9.3"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @backendfree/webanalytics
3
+ *
4
+ * Reporting page views from your own server, for the visitors a browser snippet
5
+ * never sees.
6
+ *
7
+ * import { Reporter } from '@backendfree/webanalytics';
8
+ *
9
+ * const reporter = new Reporter({ origin, key: process.env.SECRET_KEY });
10
+ *
11
+ * export function middleware(request) {
12
+ * reporter.pageview({
13
+ * url: request.url,
14
+ * referrer: request.headers.get('referer') ?? '',
15
+ * ip: request.headers.get('cf-connecting-ip') ?? '',
16
+ * userAgent: request.headers.get('user-agent') ?? '',
17
+ * optedOut: request.headers.get('sec-gpc') === '1' || request.headers.get('dnt') === '1',
18
+ * });
19
+ * return Response.next();
20
+ * }
21
+ *
22
+ * Five things are worth knowing before using it.
23
+ *
24
+ * **This needs a secret key, and it throws on a publishable one.** Every report
25
+ * asserts who the visitor was, because your server is the only thing that saw
26
+ * them. A publishable key sits in the page source of every site using it, so
27
+ * accepting one here would let anybody who read it forge visitors and spend
28
+ * your month while looking like real traffic. The platform refuses it too; this
29
+ * refuses at construction, where the stack trace points at the line that is
30
+ * wrong.
31
+ *
32
+ * **It never throws while reporting, and never delays a response.** A page must
33
+ * not fail, or wait, because measuring it failed. `pageview` and `event` return
34
+ * immediately; sending happens in the background and a failure is dropped. Call
35
+ * `flush()` where you actually want to wait, which on a serverless platform
36
+ * means inside `waitUntil`.
37
+ *
38
+ * **It is not built on `Client`.** The collector is not `/v1`: no ETags, no
39
+ * idempotency keys, no error envelope, no pagination and no request allowance.
40
+ * Routing reports through a client built for a different API would attach four
41
+ * things the endpoint ignores.
42
+ *
43
+ * **What you send about a visitor does not survive the request.** The address
44
+ * and the agent go into a hash with a salt that rotates at your project's local
45
+ * midnight and is never written down, and neither is ever stored. That is the
46
+ * same thing the browser snippet does with the address it arrives from, which
47
+ * is why sites using either need no cookie banner. A visitor who sent GPC or
48
+ * DNT is not measured at all: pass `optedOut` and the hit is never sent.
49
+ *
50
+ * **Crawlers are why this exists.** Most never run JavaScript, so the snippet
51
+ * does not see them at all. Reported from your server they are classified,
52
+ * counted and capped, and they never spend your events allowance.
53
+ */
54
+ /** Where the collector lives under an origin. */
55
+ export declare const COLLECT_PATH = "/collect/s";
56
+ /** What the platform takes in one report. Sending more is a 400. */
57
+ export declare const MAX_BATCH = 100;
58
+ /** Ten seconds, matching core's client: long enough for a cold start. */
59
+ export declare const DEFAULT_TIMEOUT = 10000;
60
+ /** How long a partly full batch waits for company before going anyway. */
61
+ export declare const DEFAULT_INTERVAL = 1000;
62
+ export interface ReporterOptions {
63
+ /** Where the platform runs, with no trailing path: `https://backendfree.com`. */
64
+ origin: string;
65
+ /** `sk_live_...`. A publishable key throws: see the note above. */
66
+ key: string;
67
+ /** Swap in your own `fetch`: a test double, or a Worker's bound fetcher. */
68
+ fetch?: typeof globalThis.fetch;
69
+ /** Per-request timeout in milliseconds. `0` disables it. */
70
+ timeout?: number;
71
+ /**
72
+ * Hits to gather before sending. 1 sends every hit on its own, which is
73
+ * simplest and costs a request per page view. Capped at `MAX_BATCH`.
74
+ */
75
+ batchSize?: number;
76
+ /**
77
+ * How long a partly full batch waits, in milliseconds. `0` sends on the next
78
+ * turn of the event loop rather than never: a site with one visitor an hour
79
+ * would otherwise hold that visit until the next one arrived.
80
+ */
81
+ interval?: number;
82
+ /**
83
+ * Called when a batch could not be sent. Nothing is retried and nothing is
84
+ * thrown, so this is the only place a failure is visible. Default: silence,
85
+ * because the alternative is an unhandled rejection taking down a worker
86
+ * over an analytics call.
87
+ */
88
+ onError?: (error: unknown) => void;
89
+ }
90
+ /** One page view or named event to report. */
91
+ export interface HitInput {
92
+ /** The full URL the visitor was on. The query is dropped except `utm_*`. */
93
+ url: string;
94
+ /** The referrer, reduced to a hostname on arrival. */
95
+ referrer?: string;
96
+ /**
97
+ * A named event, which must be one the Tracking screen declares. Anything
98
+ * else is recorded as the page view it also was.
99
+ */
100
+ name?: string;
101
+ /**
102
+ * The visitor's address, hashed on arrival and never stored. Read it from
103
+ * whatever header your proxy sets: `cf-connecting-ip`, `x-real-ip`, or the
104
+ * far end of `x-forwarded-for`. Sending your own server's address instead
105
+ * makes every visitor look like one person.
106
+ */
107
+ ip?: string;
108
+ /**
109
+ * The visitor's User-Agent. Kept only as one of three device words, and it
110
+ * is what tells a crawler from a person, so a report without it counts a
111
+ * crawler as a visitor and spends an event on it.
112
+ */
113
+ userAgent?: string;
114
+ /**
115
+ * Whether the visitor asked not to be measured: `sec-gpc: 1` or `dnt: 1` on
116
+ * their request. Only your server saw those headers, so only you can pass
117
+ * them on. An opted-out hit is dropped here and never sent, the same as the
118
+ * browser snippet does.
119
+ */
120
+ optedOut?: boolean;
121
+ }
122
+ export declare class Reporter {
123
+ #private;
124
+ readonly origin: string;
125
+ constructor(options: ReporterOptions);
126
+ /** How many hits are waiting to be sent. */
127
+ get pending(): number;
128
+ /**
129
+ * Report one page view. Returns immediately and never throws.
130
+ *
131
+ * A hit with no `url` is dropped here rather than sent: the platform would
132
+ * drop it too, for having no hostname to match against the site list, and
133
+ * spending a request to find that out helps nobody.
134
+ */
135
+ pageview(hit: HitInput): void;
136
+ /**
137
+ * Report a named event. Same rules, plus the name has to be one the Tracking
138
+ * screen declares, or it is recorded as the page view it also was.
139
+ */
140
+ event(name: string, hit: HitInput): void;
141
+ /**
142
+ * Send everything queued and wait for it.
143
+ *
144
+ * Where a serverless platform needs this: `waitUntil(reporter.flush())` keeps
145
+ * the process alive long enough for the report to leave, and without it a
146
+ * function that returns immediately takes the queue with it.
147
+ */
148
+ flush(): Promise<void>;
149
+ }
150
+ export type { Hit, ReportResult } from './types.js';
151
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAMH,iDAAiD;AACjD,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,oEAAoE;AACpE,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,yEAAyE;AACzE,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,QAAQ;;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAcZ,OAAO,EAAE,eAAe;IAiCpC,4CAA4C;IAC5C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI;IAI7B;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI;IAIxC;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmG7B;AAED,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @backendfree/webanalytics
3
+ *
4
+ * Reporting page views from your own server, for the visitors a browser snippet
5
+ * never sees.
6
+ *
7
+ * import { Reporter } from '@backendfree/webanalytics';
8
+ *
9
+ * const reporter = new Reporter({ origin, key: process.env.SECRET_KEY });
10
+ *
11
+ * export function middleware(request) {
12
+ * reporter.pageview({
13
+ * url: request.url,
14
+ * referrer: request.headers.get('referer') ?? '',
15
+ * ip: request.headers.get('cf-connecting-ip') ?? '',
16
+ * userAgent: request.headers.get('user-agent') ?? '',
17
+ * optedOut: request.headers.get('sec-gpc') === '1' || request.headers.get('dnt') === '1',
18
+ * });
19
+ * return Response.next();
20
+ * }
21
+ *
22
+ * Five things are worth knowing before using it.
23
+ *
24
+ * **This needs a secret key, and it throws on a publishable one.** Every report
25
+ * asserts who the visitor was, because your server is the only thing that saw
26
+ * them. A publishable key sits in the page source of every site using it, so
27
+ * accepting one here would let anybody who read it forge visitors and spend
28
+ * your month while looking like real traffic. The platform refuses it too; this
29
+ * refuses at construction, where the stack trace points at the line that is
30
+ * wrong.
31
+ *
32
+ * **It never throws while reporting, and never delays a response.** A page must
33
+ * not fail, or wait, because measuring it failed. `pageview` and `event` return
34
+ * immediately; sending happens in the background and a failure is dropped. Call
35
+ * `flush()` where you actually want to wait, which on a serverless platform
36
+ * means inside `waitUntil`.
37
+ *
38
+ * **It is not built on `Client`.** The collector is not `/v1`: no ETags, no
39
+ * idempotency keys, no error envelope, no pagination and no request allowance.
40
+ * Routing reports through a client built for a different API would attach four
41
+ * things the endpoint ignores.
42
+ *
43
+ * **What you send about a visitor does not survive the request.** The address
44
+ * and the agent go into a hash with a salt that rotates at your project's local
45
+ * midnight and is never written down, and neither is ever stored. That is the
46
+ * same thing the browser snippet does with the address it arrives from, which
47
+ * is why sites using either need no cookie banner. A visitor who sent GPC or
48
+ * DNT is not measured at all: pass `optedOut` and the hit is never sent.
49
+ *
50
+ * **Crawlers are why this exists.** Most never run JavaScript, so the snippet
51
+ * does not see them at all. Reported from your server they are classified,
52
+ * counted and capped, and they never spend your events allowance.
53
+ */
54
+ import { ConfigError } from '@backendfree/core';
55
+ /** Where the collector lives under an origin. */
56
+ export const COLLECT_PATH = '/collect/s';
57
+ /** What the platform takes in one report. Sending more is a 400. */
58
+ export const MAX_BATCH = 100;
59
+ /** Ten seconds, matching core's client: long enough for a cold start. */
60
+ export const DEFAULT_TIMEOUT = 10_000;
61
+ /** How long a partly full batch waits for company before going anyway. */
62
+ export const DEFAULT_INTERVAL = 1_000;
63
+ export class Reporter {
64
+ origin;
65
+ #key;
66
+ #fetch;
67
+ #timeout;
68
+ #batchSize;
69
+ #interval;
70
+ #onError;
71
+ #queue = [];
72
+ #timer = null;
73
+ /** The send in flight, so `flush()` can wait for it as well as for the queue. */
74
+ #sending = Promise.resolve();
75
+ constructor(options) {
76
+ const origin = (options.origin ?? '').trim().replace(/\/+$/, '');
77
+ if (!origin) {
78
+ throw new ConfigError('origin_required', 'Pass the origin the platform runs on.');
79
+ }
80
+ const key = (options.key ?? '').trim();
81
+ if (!key) {
82
+ throw new ConfigError('key_required', 'Pass a project API key.');
83
+ }
84
+ // The mistake this package exists to refuse. A publishable key is public by
85
+ // design, and this endpoint takes the visitor's identity on trust.
86
+ if (key.startsWith('pk_')) {
87
+ throw new ConfigError('secret_key_required', 'Reporting needs a secret key. Every report asserts who the visitor was, so a ' +
88
+ 'publishable key here would let anybody who read it out of your page source forge ' +
89
+ 'visitors and spend your events allowance.');
90
+ }
91
+ this.origin = origin;
92
+ this.#key = key;
93
+ this.#fetch = options.fetch ?? globalThis.fetch;
94
+ this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;
95
+ this.#batchSize = Math.min(Math.max(options.batchSize ?? 20, 1), MAX_BATCH);
96
+ this.#interval = Math.max(options.interval ?? DEFAULT_INTERVAL, 0);
97
+ this.#onError = options.onError ?? (() => { });
98
+ if (typeof this.#fetch !== 'function') {
99
+ throw new ConfigError('fetch_required', 'This runtime has no fetch. Pass one.');
100
+ }
101
+ }
102
+ /** How many hits are waiting to be sent. */
103
+ get pending() {
104
+ return this.#queue.length;
105
+ }
106
+ /**
107
+ * Report one page view. Returns immediately and never throws.
108
+ *
109
+ * A hit with no `url` is dropped here rather than sent: the platform would
110
+ * drop it too, for having no hostname to match against the site list, and
111
+ * spending a request to find that out helps nobody.
112
+ */
113
+ pageview(hit) {
114
+ this.#add(hit, '');
115
+ }
116
+ /**
117
+ * Report a named event. Same rules, plus the name has to be one the Tracking
118
+ * screen declares, or it is recorded as the page view it also was.
119
+ */
120
+ event(name, hit) {
121
+ this.#add(hit, name);
122
+ }
123
+ /**
124
+ * Send everything queued and wait for it.
125
+ *
126
+ * Where a serverless platform needs this: `waitUntil(reporter.flush())` keeps
127
+ * the process alive long enough for the report to leave, and without it a
128
+ * function that returns immediately takes the queue with it.
129
+ */
130
+ async flush() {
131
+ this.#cancelTimer();
132
+ while (this.#queue.length) {
133
+ await this.#send(this.#queue.splice(0, this.#batchSize));
134
+ }
135
+ await this.#sending;
136
+ }
137
+ // --- the queue ---------------------------------------------------------------
138
+ #add(hit, name) {
139
+ // Before anything else, so not even a url of theirs is queued.
140
+ if (hit?.optedOut) {
141
+ return;
142
+ }
143
+ const url = (hit?.url ?? '').trim();
144
+ if (!url) {
145
+ return;
146
+ }
147
+ this.#queue.push({
148
+ url,
149
+ referrer: hit.referrer ?? '',
150
+ name,
151
+ ip: hit.ip ?? '',
152
+ user_agent: hit.userAgent ?? '',
153
+ });
154
+ if (this.#queue.length >= this.#batchSize) {
155
+ this.#cancelTimer();
156
+ this.#launch(this.#queue.splice(0, this.#batchSize));
157
+ return;
158
+ }
159
+ this.#arm();
160
+ }
161
+ #arm() {
162
+ if (this.#timer !== null) {
163
+ return;
164
+ }
165
+ this.#timer = setTimeout(() => {
166
+ this.#timer = null;
167
+ this.#launch(this.#queue.splice(0, this.#batchSize));
168
+ }, this.#interval);
169
+ // Node keeps the process alive for a pending timer, so a script that has
170
+ // finished its work would hang for the interval waiting to report. The
171
+ // browser and Workers have no such method, hence the guard.
172
+ this.#timer?.unref?.();
173
+ }
174
+ #cancelTimer() {
175
+ if (this.#timer !== null) {
176
+ clearTimeout(this.#timer);
177
+ this.#timer = null;
178
+ }
179
+ }
180
+ /** Start a send without waiting for it, keeping it reachable for `flush()`. */
181
+ #launch(batch) {
182
+ this.#sending = this.#sending.then(() => this.#send(batch));
183
+ }
184
+ async #send(batch) {
185
+ if (!batch.length) {
186
+ return;
187
+ }
188
+ const controller = this.#timeout > 0 ? new AbortController() : null;
189
+ const timer = controller ? setTimeout(() => controller.abort(), this.#timeout) : null;
190
+ try {
191
+ const response = await this.#fetch(`${this.origin}${COLLECT_PATH}`, {
192
+ method: 'POST',
193
+ headers: {
194
+ authorization: `Bearer ${this.#key}`,
195
+ 'content-type': 'application/json',
196
+ },
197
+ body: JSON.stringify({ events: batch }),
198
+ signal: controller?.signal,
199
+ });
200
+ if (!response.ok) {
201
+ // Read, so the message says which of the four refusals it was rather
202
+ // than only the status. The platform answers a small JSON object.
203
+ const detail = await response.text().catch(() => '');
204
+ throw new Error(`The collector answered ${response.status}. ${detail}`.trim());
205
+ }
206
+ }
207
+ catch (error) {
208
+ // Swallowed on purpose, and this is the only place a failure surfaces. A
209
+ // rejected promise nobody is awaiting is an unhandled rejection, which on
210
+ // several runtimes takes the whole worker down: measuring a page must not
211
+ // be able to stop it being served.
212
+ this.#onError(error);
213
+ }
214
+ finally {
215
+ if (timer) {
216
+ clearTimeout(timer);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD,iDAAiD;AACjD,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC;AAEzC,oEAAoE;AACpE,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAE7B,yEAAyE;AACzE,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AAEtC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAgEtC,MAAM,OAAO,QAAQ;IACV,MAAM,CAAS;IAEf,IAAI,CAAS;IACb,MAAM,CAA0B;IAChC,QAAQ,CAAS;IACjB,UAAU,CAAS;IACnB,SAAS,CAAS;IAClB,QAAQ,CAA2B;IAE5C,MAAM,GAAU,EAAE,CAAC;IACnB,MAAM,GAAyC,IAAI,CAAC;IACpD,iFAAiF;IACjF,QAAQ,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE5C,YAAY,OAAwB;QAClC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,WAAW,CAAC,iBAAiB,EAAE,uCAAuC,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,WAAW,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAC;QACnE,CAAC;QACD,4EAA4E;QAC5E,mEAAmE;QACnE,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,WAAW,CACnB,qBAAqB,EACrB,+EAA+E;gBAC7E,mFAAmF;gBACnF,2CAA2C,CAC9C,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QAChD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,IAAI,eAAe,CAAC;QACnD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QAC5E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,gBAAgB,EAAE,CAAC,CAAC,CAAC;QACnE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAE9C,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACtC,MAAM,IAAI,WAAW,CAAC,gBAAgB,EAAE,sCAAsC,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAED,4CAA4C;IAC5C,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,GAAa;QACpB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAY,EAAE,GAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACvB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAC3D,CAAC;QACD,MAAM,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED,gFAAgF;IAEhF,IAAI,CAAC,GAAa,EAAE,IAAY;QAC9B,+DAA+D;QAC/D,IAAI,GAAG,EAAE,QAAQ,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACf,GAAG;YACH,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE;YAC5B,IAAI;YACJ,EAAE,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE;YAChB,UAAU,EAAE,GAAG,CAAC,SAAS,IAAI,EAAE;SAChC,CAAC,CAAC;QAEH,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1C,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YACrD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QACvD,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnB,yEAAyE;QACzE,uEAAuE;QACvE,4DAA4D;QAC5D,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;IACzB,CAAC;IAED,YAAY;QACV,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACzB,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,OAAO,CAAC,KAAY;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAY;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACpE,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEtF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,EAAE;gBAClE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,IAAI,EAAE;oBACpC,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;gBACvC,MAAM,EAAE,UAAU,EAAE,MAAM;aAC3B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,qEAAqE;gBACrE,kEAAkE;gBAClE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBACrD,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yEAAyE;YACzE,0EAA0E;YAC1E,0EAA0E;YAC1E,mCAAmC;YACnC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;gBAAS,CAAC;YACT,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What goes over the wire, in the collector's own spelling.
3
+ *
4
+ * Snake case here and camel case on `HitInput`, deliberately: the wire format
5
+ * is the platform's and changing it is a breaking change for every caller in
6
+ * every language, while the shape a TypeScript developer types is theirs. One
7
+ * conversion, in `#add`, rather than a wire format that reads as if it were
8
+ * designed for one language.
9
+ */
10
+ /** One reported hit, as `POST /collect/s` takes it. */
11
+ export interface Hit {
12
+ url: string;
13
+ referrer: string;
14
+ /** Empty for a page view. */
15
+ name: string;
16
+ /** Hashed on arrival with the day's salt, and never stored. */
17
+ ip: string;
18
+ /** Kept only as one of three device words, and used to spot a crawler. */
19
+ user_agent: string;
20
+ }
21
+ /** What the collector answers when it took the report. */
22
+ export interface ReportResult {
23
+ /** Events in the request, including any it went on to drop. */
24
+ received: number;
25
+ /**
26
+ * Events that became a row. Lower than `received` when the month's allowance
27
+ * ran out, when a hostname is not on the site list, or when the module is off.
28
+ * A crawler counts as recorded and spends no allowance.
29
+ */
30
+ recorded: number;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uDAAuD;AACvD,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB"}
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * What goes over the wire, in the collector's own spelling.
3
+ *
4
+ * Snake case here and camel case on `HitInput`, deliberately: the wire format
5
+ * is the platform's and changing it is a breaking change for every caller in
6
+ * every language, while the shape a TypeScript developer types is theirs. One
7
+ * conversion, in `#add`, rather than a wire format that reads as if it were
8
+ * designed for one language.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@backendfree/webanalytics",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Report page views to BackendFree from your own server, for the visitors and crawlers a browser snippet never sees.",
5
+ "keywords": [
6
+ "backendfree",
7
+ "analytics",
8
+ "cookieless",
9
+ "privacy",
10
+ "sdk",
11
+ "typescript"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "BackendFree",
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "README.md"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@backendfree/core": "^0.1.0"
39
+ },
40
+ "scripts": {
41
+ "clean": "tsc --build --clean && rm -rf dist",
42
+ "build": "tsc --build",
43
+ "test": "npm run build && node --test test/*.test.mjs",
44
+ "prepare": "npm run build",
45
+ "prepublishOnly": "npm test"
46
+ }
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @backendfree/webanalytics
3
+ *
4
+ * Reporting page views from your own server, for the visitors a browser snippet
5
+ * never sees.
6
+ *
7
+ * import { Reporter } from '@backendfree/webanalytics';
8
+ *
9
+ * const reporter = new Reporter({ origin, key: process.env.SECRET_KEY });
10
+ *
11
+ * export function middleware(request) {
12
+ * reporter.pageview({
13
+ * url: request.url,
14
+ * referrer: request.headers.get('referer') ?? '',
15
+ * ip: request.headers.get('cf-connecting-ip') ?? '',
16
+ * userAgent: request.headers.get('user-agent') ?? '',
17
+ * optedOut: request.headers.get('sec-gpc') === '1' || request.headers.get('dnt') === '1',
18
+ * });
19
+ * return Response.next();
20
+ * }
21
+ *
22
+ * Five things are worth knowing before using it.
23
+ *
24
+ * **This needs a secret key, and it throws on a publishable one.** Every report
25
+ * asserts who the visitor was, because your server is the only thing that saw
26
+ * them. A publishable key sits in the page source of every site using it, so
27
+ * accepting one here would let anybody who read it forge visitors and spend
28
+ * your month while looking like real traffic. The platform refuses it too; this
29
+ * refuses at construction, where the stack trace points at the line that is
30
+ * wrong.
31
+ *
32
+ * **It never throws while reporting, and never delays a response.** A page must
33
+ * not fail, or wait, because measuring it failed. `pageview` and `event` return
34
+ * immediately; sending happens in the background and a failure is dropped. Call
35
+ * `flush()` where you actually want to wait, which on a serverless platform
36
+ * means inside `waitUntil`.
37
+ *
38
+ * **It is not built on `Client`.** The collector is not `/v1`: no ETags, no
39
+ * idempotency keys, no error envelope, no pagination and no request allowance.
40
+ * Routing reports through a client built for a different API would attach four
41
+ * things the endpoint ignores.
42
+ *
43
+ * **What you send about a visitor does not survive the request.** The address
44
+ * and the agent go into a hash with a salt that rotates at your project's local
45
+ * midnight and is never written down, and neither is ever stored. That is the
46
+ * same thing the browser snippet does with the address it arrives from, which
47
+ * is why sites using either need no cookie banner. A visitor who sent GPC or
48
+ * DNT is not measured at all: pass `optedOut` and the hit is never sent.
49
+ *
50
+ * **Crawlers are why this exists.** Most never run JavaScript, so the snippet
51
+ * does not see them at all. Reported from your server they are classified,
52
+ * counted and capped, and they never spend your events allowance.
53
+ */
54
+
55
+ import { ConfigError } from '@backendfree/core';
56
+
57
+ import type { Hit, ReportResult } from './types.js';
58
+
59
+ /** Where the collector lives under an origin. */
60
+ export const COLLECT_PATH = '/collect/s';
61
+
62
+ /** What the platform takes in one report. Sending more is a 400. */
63
+ export const MAX_BATCH = 100;
64
+
65
+ /** Ten seconds, matching core's client: long enough for a cold start. */
66
+ export const DEFAULT_TIMEOUT = 10_000;
67
+
68
+ /** How long a partly full batch waits for company before going anyway. */
69
+ export const DEFAULT_INTERVAL = 1_000;
70
+
71
+ export interface ReporterOptions {
72
+ /** Where the platform runs, with no trailing path: `https://backendfree.com`. */
73
+ origin: string;
74
+ /** `sk_live_...`. A publishable key throws: see the note above. */
75
+ key: string;
76
+ /** Swap in your own `fetch`: a test double, or a Worker's bound fetcher. */
77
+ fetch?: typeof globalThis.fetch;
78
+ /** Per-request timeout in milliseconds. `0` disables it. */
79
+ timeout?: number;
80
+ /**
81
+ * Hits to gather before sending. 1 sends every hit on its own, which is
82
+ * simplest and costs a request per page view. Capped at `MAX_BATCH`.
83
+ */
84
+ batchSize?: number;
85
+ /**
86
+ * How long a partly full batch waits, in milliseconds. `0` sends on the next
87
+ * turn of the event loop rather than never: a site with one visitor an hour
88
+ * would otherwise hold that visit until the next one arrived.
89
+ */
90
+ interval?: number;
91
+ /**
92
+ * Called when a batch could not be sent. Nothing is retried and nothing is
93
+ * thrown, so this is the only place a failure is visible. Default: silence,
94
+ * because the alternative is an unhandled rejection taking down a worker
95
+ * over an analytics call.
96
+ */
97
+ onError?: (error: unknown) => void;
98
+ }
99
+
100
+ /** One page view or named event to report. */
101
+ export interface HitInput {
102
+ /** The full URL the visitor was on. The query is dropped except `utm_*`. */
103
+ url: string;
104
+ /** The referrer, reduced to a hostname on arrival. */
105
+ referrer?: string;
106
+ /**
107
+ * A named event, which must be one the Tracking screen declares. Anything
108
+ * else is recorded as the page view it also was.
109
+ */
110
+ name?: string;
111
+ /**
112
+ * The visitor's address, hashed on arrival and never stored. Read it from
113
+ * whatever header your proxy sets: `cf-connecting-ip`, `x-real-ip`, or the
114
+ * far end of `x-forwarded-for`. Sending your own server's address instead
115
+ * makes every visitor look like one person.
116
+ */
117
+ ip?: string;
118
+ /**
119
+ * The visitor's User-Agent. Kept only as one of three device words, and it
120
+ * is what tells a crawler from a person, so a report without it counts a
121
+ * crawler as a visitor and spends an event on it.
122
+ */
123
+ userAgent?: string;
124
+ /**
125
+ * Whether the visitor asked not to be measured: `sec-gpc: 1` or `dnt: 1` on
126
+ * their request. Only your server saw those headers, so only you can pass
127
+ * them on. An opted-out hit is dropped here and never sent, the same as the
128
+ * browser snippet does.
129
+ */
130
+ optedOut?: boolean;
131
+ }
132
+
133
+ export class Reporter {
134
+ readonly origin: string;
135
+
136
+ readonly #key: string;
137
+ readonly #fetch: typeof globalThis.fetch;
138
+ readonly #timeout: number;
139
+ readonly #batchSize: number;
140
+ readonly #interval: number;
141
+ readonly #onError: (error: unknown) => void;
142
+
143
+ #queue: Hit[] = [];
144
+ #timer: ReturnType<typeof setTimeout> | null = null;
145
+ /** The send in flight, so `flush()` can wait for it as well as for the queue. */
146
+ #sending: Promise<void> = Promise.resolve();
147
+
148
+ constructor(options: ReporterOptions) {
149
+ const origin = (options.origin ?? '').trim().replace(/\/+$/, '');
150
+ if (!origin) {
151
+ throw new ConfigError('origin_required', 'Pass the origin the platform runs on.');
152
+ }
153
+ const key = (options.key ?? '').trim();
154
+ if (!key) {
155
+ throw new ConfigError('key_required', 'Pass a project API key.');
156
+ }
157
+ // The mistake this package exists to refuse. A publishable key is public by
158
+ // design, and this endpoint takes the visitor's identity on trust.
159
+ if (key.startsWith('pk_')) {
160
+ throw new ConfigError(
161
+ 'secret_key_required',
162
+ 'Reporting needs a secret key. Every report asserts who the visitor was, so a ' +
163
+ 'publishable key here would let anybody who read it out of your page source forge ' +
164
+ 'visitors and spend your events allowance.',
165
+ );
166
+ }
167
+
168
+ this.origin = origin;
169
+ this.#key = key;
170
+ this.#fetch = options.fetch ?? globalThis.fetch;
171
+ this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;
172
+ this.#batchSize = Math.min(Math.max(options.batchSize ?? 20, 1), MAX_BATCH);
173
+ this.#interval = Math.max(options.interval ?? DEFAULT_INTERVAL, 0);
174
+ this.#onError = options.onError ?? (() => {});
175
+
176
+ if (typeof this.#fetch !== 'function') {
177
+ throw new ConfigError('fetch_required', 'This runtime has no fetch. Pass one.');
178
+ }
179
+ }
180
+
181
+ /** How many hits are waiting to be sent. */
182
+ get pending(): number {
183
+ return this.#queue.length;
184
+ }
185
+
186
+ /**
187
+ * Report one page view. Returns immediately and never throws.
188
+ *
189
+ * A hit with no `url` is dropped here rather than sent: the platform would
190
+ * drop it too, for having no hostname to match against the site list, and
191
+ * spending a request to find that out helps nobody.
192
+ */
193
+ pageview(hit: HitInput): void {
194
+ this.#add(hit, '');
195
+ }
196
+
197
+ /**
198
+ * Report a named event. Same rules, plus the name has to be one the Tracking
199
+ * screen declares, or it is recorded as the page view it also was.
200
+ */
201
+ event(name: string, hit: HitInput): void {
202
+ this.#add(hit, name);
203
+ }
204
+
205
+ /**
206
+ * Send everything queued and wait for it.
207
+ *
208
+ * Where a serverless platform needs this: `waitUntil(reporter.flush())` keeps
209
+ * the process alive long enough for the report to leave, and without it a
210
+ * function that returns immediately takes the queue with it.
211
+ */
212
+ async flush(): Promise<void> {
213
+ this.#cancelTimer();
214
+ while (this.#queue.length) {
215
+ await this.#send(this.#queue.splice(0, this.#batchSize));
216
+ }
217
+ await this.#sending;
218
+ }
219
+
220
+ // --- the queue ---------------------------------------------------------------
221
+
222
+ #add(hit: HitInput, name: string): void {
223
+ // Before anything else, so not even a url of theirs is queued.
224
+ if (hit?.optedOut) {
225
+ return;
226
+ }
227
+ const url = (hit?.url ?? '').trim();
228
+ if (!url) {
229
+ return;
230
+ }
231
+
232
+ this.#queue.push({
233
+ url,
234
+ referrer: hit.referrer ?? '',
235
+ name,
236
+ ip: hit.ip ?? '',
237
+ user_agent: hit.userAgent ?? '',
238
+ });
239
+
240
+ if (this.#queue.length >= this.#batchSize) {
241
+ this.#cancelTimer();
242
+ this.#launch(this.#queue.splice(0, this.#batchSize));
243
+ return;
244
+ }
245
+ this.#arm();
246
+ }
247
+
248
+ #arm(): void {
249
+ if (this.#timer !== null) {
250
+ return;
251
+ }
252
+ this.#timer = setTimeout(() => {
253
+ this.#timer = null;
254
+ this.#launch(this.#queue.splice(0, this.#batchSize));
255
+ }, this.#interval);
256
+ // Node keeps the process alive for a pending timer, so a script that has
257
+ // finished its work would hang for the interval waiting to report. The
258
+ // browser and Workers have no such method, hence the guard.
259
+ this.#timer?.unref?.();
260
+ }
261
+
262
+ #cancelTimer(): void {
263
+ if (this.#timer !== null) {
264
+ clearTimeout(this.#timer);
265
+ this.#timer = null;
266
+ }
267
+ }
268
+
269
+ /** Start a send without waiting for it, keeping it reachable for `flush()`. */
270
+ #launch(batch: Hit[]): void {
271
+ this.#sending = this.#sending.then(() => this.#send(batch));
272
+ }
273
+
274
+ async #send(batch: Hit[]): Promise<void> {
275
+ if (!batch.length) {
276
+ return;
277
+ }
278
+
279
+ const controller = this.#timeout > 0 ? new AbortController() : null;
280
+ const timer = controller ? setTimeout(() => controller.abort(), this.#timeout) : null;
281
+
282
+ try {
283
+ const response = await this.#fetch(`${this.origin}${COLLECT_PATH}`, {
284
+ method: 'POST',
285
+ headers: {
286
+ authorization: `Bearer ${this.#key}`,
287
+ 'content-type': 'application/json',
288
+ },
289
+ body: JSON.stringify({ events: batch }),
290
+ signal: controller?.signal,
291
+ });
292
+
293
+ if (!response.ok) {
294
+ // Read, so the message says which of the four refusals it was rather
295
+ // than only the status. The platform answers a small JSON object.
296
+ const detail = await response.text().catch(() => '');
297
+ throw new Error(`The collector answered ${response.status}. ${detail}`.trim());
298
+ }
299
+ } catch (error) {
300
+ // Swallowed on purpose, and this is the only place a failure surfaces. A
301
+ // rejected promise nobody is awaiting is an unhandled rejection, which on
302
+ // several runtimes takes the whole worker down: measuring a page must not
303
+ // be able to stop it being served.
304
+ this.#onError(error);
305
+ } finally {
306
+ if (timer) {
307
+ clearTimeout(timer);
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ export type { Hit, ReportResult } from './types.js';
package/src/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What goes over the wire, in the collector's own spelling.
3
+ *
4
+ * Snake case here and camel case on `HitInput`, deliberately: the wire format
5
+ * is the platform's and changing it is a breaking change for every caller in
6
+ * every language, while the shape a TypeScript developer types is theirs. One
7
+ * conversion, in `#add`, rather than a wire format that reads as if it were
8
+ * designed for one language.
9
+ */
10
+
11
+ /** One reported hit, as `POST /collect/s` takes it. */
12
+ export interface Hit {
13
+ url: string;
14
+ referrer: string;
15
+ /** Empty for a page view. */
16
+ name: string;
17
+ /** Hashed on arrival with the day's salt, and never stored. */
18
+ ip: string;
19
+ /** Kept only as one of three device words, and used to spot a crawler. */
20
+ user_agent: string;
21
+ }
22
+
23
+ /** What the collector answers when it took the report. */
24
+ export interface ReportResult {
25
+ /** Events in the request, including any it went on to drop. */
26
+ received: number;
27
+ /**
28
+ * Events that became a row. Lower than `received` when the month's allowance
29
+ * ran out, when a hostname is not on the site list, or when the module is off.
30
+ * A crawler counts as recorded and spends no allowance.
31
+ */
32
+ recorded: number;
33
+ }