@huloglobal/vendure-plugin-visitor-analytics 0.2.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/CHANGELOG.md +39 -0
- package/LICENSE +34 -0
- package/README.md +167 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +47 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +96 -0
- package/dist/plugin.js.map +1 -0
- package/dist/proxy-headers.d.ts +37 -0
- package/dist/proxy-headers.d.ts.map +1 -0
- package/dist/proxy-headers.js +81 -0
- package/dist/proxy-headers.js.map +1 -0
- package/dist/visitor-event.entity.d.ts +68 -0
- package/dist/visitor-event.entity.d.ts.map +1 -0
- package/dist/visitor-event.entity.js +163 -0
- package/dist/visitor-event.entity.js.map +1 -0
- package/dist/visitor-tracking.controller.d.ts +66 -0
- package/dist/visitor-tracking.controller.d.ts.map +1 -0
- package/dist/visitor-tracking.controller.js +658 -0
- package/dist/visitor-tracking.controller.js.map +1 -0
- package/dist/visitor-tracking.service.d.ts +41 -0
- package/dist/visitor-tracking.service.d.ts.map +1 -0
- package/dist/visitor-tracking.service.js +251 -0
- package/dist/visitor-tracking.service.js.map +1 -0
- package/package.json +51 -0
- package/ui/components/visitors.component.ts +695 -0
- package/ui/visitors.module.ts +17 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@huloglobal/vendure-plugin-visitor-analytics` are
|
|
4
|
+
documented here. The format follows
|
|
5
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this
|
|
6
|
+
project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] — Unreleased
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **UTM attribution** — `utmSource` / `utmMedium` / `utmCampaign` /
|
|
12
|
+
`utmTerm` / `utmContent` and `referrerDomain` columns parsed from every
|
|
13
|
+
incoming pageview URL. New `GET /ees/visitors/sources` admin endpoint
|
|
14
|
+
groups visitors by `(source, medium)` plus per-source conversion
|
|
15
|
+
counts (reached product page, reached cart/checkout).
|
|
16
|
+
- **Live-now widget** — Server-Sent Events stream at
|
|
17
|
+
`GET /ees/visitors/live` pushing the active-visitor count and the
|
|
18
|
+
20 most recent URLs every 5 seconds. SSE clients auto-reconnect.
|
|
19
|
+
- **Top events** admin endpoint at `GET /ees/visitors/top-events`,
|
|
20
|
+
paginated.
|
|
21
|
+
- **Custom event recipes** — README section with copy-paste storefront
|
|
22
|
+
snippets for add-to-cart, search, quote-request, newsletter signup.
|
|
23
|
+
|
|
24
|
+
## [0.1.0] — Unreleased
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `VisitorAnalyticsPlugin` — ingest endpoint + admin dashboards.
|
|
28
|
+
- `VisitorEvent` entity capturing pageview / unload / event rows with
|
|
29
|
+
full UA parse, MaxMind geo enrichment, raw + hashed IP.
|
|
30
|
+
- Proxy-aware IP / country / region extraction (Cloudflare, Akamai,
|
|
31
|
+
Fastly headers all detected; falls back to MaxMind only if the
|
|
32
|
+
upstream didn't already resolve country).
|
|
33
|
+
- Admin endpoints: summary, top pages (paginated), exit pages
|
|
34
|
+
(paginated), funnel, recent visitors (paginated), per-visitor
|
|
35
|
+
profile + journey.
|
|
36
|
+
- Admin UI: summary tiles, funnel bars, top + exit page tables,
|
|
37
|
+
recent visitors table with clickable profile drawer.
|
|
38
|
+
- Licence verification via `@huloglobal/vendure-licence-sdk` with revocation
|
|
39
|
+
polling.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
HULO Global Limited — Commercial Plugin Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HULO Global Limited. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is licensed, not sold, and is made available only to users
|
|
6
|
+
who hold a valid, active subscription or perpetual licence purchased
|
|
7
|
+
from HULO Global Limited.
|
|
8
|
+
|
|
9
|
+
Permitted use:
|
|
10
|
+
|
|
11
|
+
1. The software may be installed and run on a single Vendure
|
|
12
|
+
instance per active subscription (or per perpetual licence). Each
|
|
13
|
+
licence is bound to one or more hostnames named at purchase time.
|
|
14
|
+
|
|
15
|
+
2. The source code is published only so that customers may audit it
|
|
16
|
+
for security review. Modification of the source for production use
|
|
17
|
+
is permitted, but redistribution of the modified source or of any
|
|
18
|
+
derivative work — in whole or in part — is not permitted.
|
|
19
|
+
|
|
20
|
+
Prohibited use:
|
|
21
|
+
|
|
22
|
+
- Use without a valid, active licence key, except for non-production
|
|
23
|
+
evaluation under the plugin's "unlicensed mode" (which writes basic
|
|
24
|
+
delivery rows but disables the open/click endpoints).
|
|
25
|
+
- Use of the package to circumvent the licence verification routine,
|
|
26
|
+
or to extract or replace the embedded RSA public key.
|
|
27
|
+
- Resale, sublicensing, or distribution of the package outside of an
|
|
28
|
+
application that itself holds a valid HULO Vendure plugin licence.
|
|
29
|
+
|
|
30
|
+
No warranty. To the maximum extent permitted by law, HULO Global Limited
|
|
31
|
+
disclaims all warranties and all liability arising from use of this
|
|
32
|
+
software.
|
|
33
|
+
|
|
34
|
+
For commercial licensing enquiries: sales@eliteenterprisesoftware.com
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @huloglobal/vendure-plugin-visitor-analytics
|
|
2
|
+
|
|
3
|
+
Full-funnel visitor analytics for Vendure storefronts. Captures every
|
|
4
|
+
pageview, time-on-page, and exit point; bundles a per-visitor profile
|
|
5
|
+
drawer with parsed user-agent, MaxMind geo enrichment and a complete
|
|
6
|
+
event timeline; stitches guest browsing to signed-in browsing across
|
|
7
|
+
sessions so the journey survives login.
|
|
8
|
+
|
|
9
|
+
Maintained by Wayne Garrison.
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
- **Ingest endpoint** at `POST /ees/track` that takes a small batch of
|
|
14
|
+
events from the storefront. Issues a long-lived visitor cookie
|
|
15
|
+
(`ees_vid`, 2 years) and a sliding session cookie (`ees_sid`,
|
|
16
|
+
30-minute idle).
|
|
17
|
+
- **Auto-enrichment** at ingest time:
|
|
18
|
+
- User-agent parsed via `ua-parser-js` → browser / browser version /
|
|
19
|
+
OS / OS version / device type. Bots auto-detected.
|
|
20
|
+
- IP-to-geo via MaxMind GeoLite2-City (no MaxMind account required,
|
|
21
|
+
DB fetched at install via `geolite2-redist`). Or use the upstream
|
|
22
|
+
proxy's resolved country / region when Cloudflare / Akamai /
|
|
23
|
+
Fastly is in front — saves the lookup.
|
|
24
|
+
- Raw IP is kept; a SHA-256 salted hash is stored alongside for
|
|
25
|
+
spot-the-same-bot work.
|
|
26
|
+
- **Admin endpoints**: summary tiles, top pages, exit pages, funnel,
|
|
27
|
+
recent visitors, per-visitor profile + journey timeline. All
|
|
28
|
+
paginated.
|
|
29
|
+
- **Admin UI**: top-line tiles, funnel bars, top + exit page tables,
|
|
30
|
+
recent visitors with a clickable profile drawer showing every field
|
|
31
|
+
+ per-session breakdown + the full event timeline.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
yarn add @huloglobal/vendure-plugin-visitor-analytics
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Wire up
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { VisitorAnalyticsPlugin } from '@huloglobal/vendure-plugin-visitor-analytics';
|
|
43
|
+
|
|
44
|
+
export const config: VendureConfig = {
|
|
45
|
+
plugins: [
|
|
46
|
+
VisitorAnalyticsPlugin.init({
|
|
47
|
+
publicBaseUrl: 'https://shop.example.com',
|
|
48
|
+
licenceKey: process.env.HULO_LICENCE_KEY,
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add to your admin-ui compile step:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { VisitorAnalyticsPlugin } from '@huloglobal/vendure-plugin-visitor-analytics';
|
|
58
|
+
|
|
59
|
+
compileUiExtensions({
|
|
60
|
+
outputPath: 'admin-ui',
|
|
61
|
+
extensions: [VisitorAnalyticsPlugin.uiExtensions /* + your other extensions */],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Storefront integration
|
|
66
|
+
|
|
67
|
+
The plugin ships only the backend; the storefront emits events. A Qwik
|
|
68
|
+
storefront example:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// utils/tracker.ts
|
|
72
|
+
const TRACK_URL = 'https://shop.example.com/ees/track';
|
|
73
|
+
let lastPath = '';
|
|
74
|
+
let pageOpenedAt = 0;
|
|
75
|
+
|
|
76
|
+
export function recordPageView(): void {
|
|
77
|
+
const url = location.pathname + location.search;
|
|
78
|
+
const events: any[] = [];
|
|
79
|
+
if (lastPath && lastPath !== url) {
|
|
80
|
+
events.push({ type: 'unload', url: lastPath, timeOnPageMs: Date.now() - pageOpenedAt });
|
|
81
|
+
}
|
|
82
|
+
events.push({ type: 'pageview', url, title: document.title, referrer: document.referrer });
|
|
83
|
+
lastPath = url;
|
|
84
|
+
pageOpenedAt = Date.now();
|
|
85
|
+
fetch(TRACK_URL, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
credentials: 'include',
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ channelId: 1, events }),
|
|
90
|
+
keepalive: true,
|
|
91
|
+
}).catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// On unload, prefer sendBeacon — it survives tab-close.
|
|
95
|
+
window.addEventListener('pagehide', () => {
|
|
96
|
+
const blob = new Blob([JSON.stringify({
|
|
97
|
+
channelId: 1,
|
|
98
|
+
events: [{ type: 'unload', url: lastPath, timeOnPageMs: Date.now() - pageOpenedAt }],
|
|
99
|
+
})], { type: 'application/json' });
|
|
100
|
+
navigator.sendBeacon(TRACK_URL, blob);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Custom events
|
|
105
|
+
|
|
106
|
+
Fire a `recordEvent(type, meta?)` call from your storefront whenever a
|
|
107
|
+
visitor does something interesting — add-to-cart, search, signup,
|
|
108
|
+
quote-request — and the event shows up in the admin "Top events" table
|
|
109
|
+
straight away.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
function recordEvent(type, meta) {
|
|
113
|
+
navigator.sendBeacon(TRACK_URL, new Blob([JSON.stringify({
|
|
114
|
+
channelId: 1,
|
|
115
|
+
events: [{ type, url: location.pathname + location.search, meta }],
|
|
116
|
+
})], { type: 'application/json' }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Add to cart
|
|
120
|
+
recordEvent('add_to_cart', { sku: 'WIN11-PRO', priceWithTax: 28900 });
|
|
121
|
+
|
|
122
|
+
// Search query submitted
|
|
123
|
+
recordEvent('search', { query: 'windows server 2022' });
|
|
124
|
+
|
|
125
|
+
// Quote request from the contact form
|
|
126
|
+
recordEvent('quote_request', { customerEmail });
|
|
127
|
+
|
|
128
|
+
// Newsletter signup
|
|
129
|
+
recordEvent('newsletter_signup', { source: 'footer' });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The full meta blob is persisted as JSON on the event row so you can
|
|
133
|
+
slice on it later from the admin UI.
|
|
134
|
+
|
|
135
|
+
## UTM attribution
|
|
136
|
+
|
|
137
|
+
The plugin parses `utm_source` / `utm_medium` / `utm_campaign` /
|
|
138
|
+
`utm_term` / `utm_content` from the URL of every incoming event and
|
|
139
|
+
captures the referrer domain alongside. The admin "Traffic sources"
|
|
140
|
+
table groups visitors by `(source, medium)` so you can see which
|
|
141
|
+
campaigns convert.
|
|
142
|
+
|
|
143
|
+
Drop `?utm_source=google&utm_medium=cpc&utm_campaign=spring24` onto any
|
|
144
|
+
inbound link and it surfaces automatically — no extra config.
|
|
145
|
+
|
|
146
|
+
## Live now widget
|
|
147
|
+
|
|
148
|
+
The admin dashboard's top tile streams the currently-active visitor
|
|
149
|
+
count + the URLs they're on in real time via Server-Sent Events
|
|
150
|
+
(`GET /ees/visitors/live`). Updates every 5 seconds, reconnects
|
|
151
|
+
automatically if the connection drops. Active = at least one event in
|
|
152
|
+
the last 5 minutes.
|
|
153
|
+
|
|
154
|
+
## Init options
|
|
155
|
+
|
|
156
|
+
| Option | Type | Required | Description |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `publicBaseUrl` | `string` | yes | Public hostname of your Vendure server (must match licence). |
|
|
159
|
+
| `licenceKey` | `string` | no* | JWT licence key. Without it ingest works but the admin dashboards return 403. |
|
|
160
|
+
|
|
161
|
+
\* Required for production use. Buy at
|
|
162
|
+
`https://elite-software.co.uk/licence/buy/vendure-plugin-visitor-analytics`.
|
|
163
|
+
|
|
164
|
+
## Licence
|
|
165
|
+
|
|
166
|
+
Commercial — see [LICENSE](./LICENSE). Requires an active subscription
|
|
167
|
+
($9.95/mo) or a perpetual licence.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huloglobal/vendure-plugin-visitor-analytics` — public exports.
|
|
3
|
+
*/
|
|
4
|
+
export { VisitorAnalyticsPlugin, VisitorAnalyticsPluginOptions } from './plugin';
|
|
5
|
+
export { VisitorTrackingService } from './visitor-tracking.service';
|
|
6
|
+
export { VisitorEvent } from './visitor-event.entity';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@huloglobal/vendure-plugin-visitor-analytics` — public exports.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VisitorEvent = exports.VisitorTrackingService = exports.VisitorAnalyticsPlugin = void 0;
|
|
7
|
+
var plugin_1 = require("./plugin");
|
|
8
|
+
Object.defineProperty(exports, "VisitorAnalyticsPlugin", { enumerable: true, get: function () { return plugin_1.VisitorAnalyticsPlugin; } });
|
|
9
|
+
var visitor_tracking_service_1 = require("./visitor-tracking.service");
|
|
10
|
+
Object.defineProperty(exports, "VisitorTrackingService", { enumerable: true, get: function () { return visitor_tracking_service_1.VisitorTrackingService; } });
|
|
11
|
+
var visitor_event_entity_1 = require("./visitor-event.entity");
|
|
12
|
+
Object.defineProperty(exports, "VisitorEvent", { enumerable: true, get: function () { return visitor_event_entity_1.VisitorEvent; } });
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,mCAAiF;AAAxE,gHAAA,sBAAsB,OAAA;AAC/B,uEAAoE;AAA3D,kIAAA,sBAAsB,OAAA;AAC/B,+DAAsD;AAA7C,oHAAA,YAAY,OAAA"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Type } from '@vendure/core';
|
|
2
|
+
export interface VisitorAnalyticsPluginOptions {
|
|
3
|
+
/** Public host of the Vendure server. Used in licence host-match. */
|
|
4
|
+
publicBaseUrl: string;
|
|
5
|
+
/** JWT licence key. Without it the ingest endpoint accepts events
|
|
6
|
+
* and writes basic rows, but the admin analytics endpoints return
|
|
7
|
+
* 403 — i.e. data collection works for evaluation but you can't
|
|
8
|
+
* read the dashboard without a licence. */
|
|
9
|
+
licenceKey?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `@huloglobal/vendure-plugin-visitor-analytics`
|
|
13
|
+
*
|
|
14
|
+
* Full-funnel visitor analytics: page views, time-on-page, exit pages,
|
|
15
|
+
* a configurable funnel, and a per-visitor profile drawer with
|
|
16
|
+
* parsed user-agent + MaxMind GeoLite2 geo enrichment. Survives login
|
|
17
|
+
* — guest events and signed-in events share the same `visitorId`.
|
|
18
|
+
*
|
|
19
|
+
* Add to your Vendure config:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { VisitorAnalyticsPlugin } from '@huloglobal/vendure-plugin-visitor-analytics';
|
|
23
|
+
*
|
|
24
|
+
* export const config: VendureConfig = {
|
|
25
|
+
* plugins: [
|
|
26
|
+
* VisitorAnalyticsPlugin.init({
|
|
27
|
+
* publicBaseUrl: 'https://shop.example.com',
|
|
28
|
+
* licenceKey: process.env.HULO_LICENCE_KEY,
|
|
29
|
+
* }),
|
|
30
|
+
* ],
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class VisitorAnalyticsPlugin {
|
|
35
|
+
private static revocation;
|
|
36
|
+
static init(options: VisitorAnalyticsPluginOptions): Type<VisitorAnalyticsPlugin>;
|
|
37
|
+
static uiExtensions: {
|
|
38
|
+
extensionPath: string;
|
|
39
|
+
ngModules: {
|
|
40
|
+
type: "lazy";
|
|
41
|
+
route: string;
|
|
42
|
+
ngModuleFileName: string;
|
|
43
|
+
ngModuleName: string;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,IAAI,EAAiB,MAAM,eAAe,CAAC;AAMxE,MAAM,WAAW,6BAA6B;IAC1C,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB;;;gDAG4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAgBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAOa,sBAAsB;IAC/B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkC;IAE3D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAAC,sBAAsB,CAAC;IA2BjF,MAAM,CAAC,YAAY;;;;;;;;MAUjB;CACL"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var VisitorAnalyticsPlugin_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.VisitorAnalyticsPlugin = void 0;
|
|
11
|
+
const core_1 = require("@vendure/core");
|
|
12
|
+
const vendure_licence_sdk_1 = require("@huloglobal/vendure-licence-sdk");
|
|
13
|
+
const visitor_event_entity_1 = require("./visitor-event.entity");
|
|
14
|
+
const visitor_tracking_service_1 = require("./visitor-tracking.service");
|
|
15
|
+
const visitor_tracking_controller_1 = require("./visitor-tracking.controller");
|
|
16
|
+
const HULO_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
17
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoLmNM5UljRqe71drM6lR
|
|
18
|
+
Ba5vXrLOcV3GAHkYvnVFQSqdE0avrge/jsD7WdA6x8qQFNRugxQcxDJa2l0+C+BH
|
|
19
|
+
SbU9TimGwhA1yusHHfuz9LAXks5IQ48+2e6Pulh7iThXPJUnIKqKZUN5HhL79aaK
|
|
20
|
+
vrZKIgSfVhwE5PMPXWZ+Ij5IRf74PLIUn1Er75qhBXlDJ4vF8y8/3owURNC1XiUB
|
|
21
|
+
DGElwV/LYNoqAQei4oixe4EAxPGvFi11pgHiGuRxuWckA88y6ZHLt6urfAY9sCkj
|
|
22
|
+
kF+2dc2yS3j7lD+SYAaV5LQYYjePP1CYvxCZ7HHRKqthHopxY1hsK2tBtni3f7/c
|
|
23
|
+
UwIDAQAB
|
|
24
|
+
-----END PUBLIC KEY-----`;
|
|
25
|
+
const PLUGIN_ID = 'vendure-plugin-visitor-analytics';
|
|
26
|
+
const REVOCATION_URL = process.env.HULO_LICENCE_REVOCATION_URL
|
|
27
|
+
|| 'https://elite.charity/licence/revoked.json';
|
|
28
|
+
/**
|
|
29
|
+
* `@huloglobal/vendure-plugin-visitor-analytics`
|
|
30
|
+
*
|
|
31
|
+
* Full-funnel visitor analytics: page views, time-on-page, exit pages,
|
|
32
|
+
* a configurable funnel, and a per-visitor profile drawer with
|
|
33
|
+
* parsed user-agent + MaxMind GeoLite2 geo enrichment. Survives login
|
|
34
|
+
* — guest events and signed-in events share the same `visitorId`.
|
|
35
|
+
*
|
|
36
|
+
* Add to your Vendure config:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { VisitorAnalyticsPlugin } from '@huloglobal/vendure-plugin-visitor-analytics';
|
|
40
|
+
*
|
|
41
|
+
* export const config: VendureConfig = {
|
|
42
|
+
* plugins: [
|
|
43
|
+
* VisitorAnalyticsPlugin.init({
|
|
44
|
+
* publicBaseUrl: 'https://shop.example.com',
|
|
45
|
+
* licenceKey: process.env.HULO_LICENCE_KEY,
|
|
46
|
+
* }),
|
|
47
|
+
* ],
|
|
48
|
+
* };
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
let VisitorAnalyticsPlugin = VisitorAnalyticsPlugin_1 = class VisitorAnalyticsPlugin {
|
|
52
|
+
static init(options) {
|
|
53
|
+
if (!VisitorAnalyticsPlugin_1.revocation) {
|
|
54
|
+
VisitorAnalyticsPlugin_1.revocation = new vendure_licence_sdk_1.RevocationChecker(REVOCATION_URL);
|
|
55
|
+
VisitorAnalyticsPlugin_1.revocation.start();
|
|
56
|
+
}
|
|
57
|
+
const host = (options.publicBaseUrl || '')
|
|
58
|
+
.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
59
|
+
const status = (0, vendure_licence_sdk_1.verifyLicence)({
|
|
60
|
+
licenceKey: options.licenceKey,
|
|
61
|
+
pluginId: PLUGIN_ID,
|
|
62
|
+
host,
|
|
63
|
+
publicKey: HULO_PUBLIC_KEY,
|
|
64
|
+
revokedIds: VisitorAnalyticsPlugin_1.revocation.getRevokedIds(),
|
|
65
|
+
});
|
|
66
|
+
if (!status.valid) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.warn(`[@huloglobal/vendure-plugin-visitor-analytics] ${status.message}` +
|
|
69
|
+
` — Running in unlicensed mode (ingest works, admin dashboards disabled). Purchase a key at https://elite-software.co.uk/licence/buy/${PLUGIN_ID}`);
|
|
70
|
+
}
|
|
71
|
+
return VisitorAnalyticsPlugin_1;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
exports.VisitorAnalyticsPlugin = VisitorAnalyticsPlugin;
|
|
75
|
+
VisitorAnalyticsPlugin.revocation = null;
|
|
76
|
+
VisitorAnalyticsPlugin.uiExtensions = {
|
|
77
|
+
extensionPath: __dirname + '/../ui',
|
|
78
|
+
ngModules: [
|
|
79
|
+
{
|
|
80
|
+
type: 'lazy',
|
|
81
|
+
route: 'visitors',
|
|
82
|
+
ngModuleFileName: 'visitors.module.ts',
|
|
83
|
+
ngModuleName: 'VisitorsModule',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
exports.VisitorAnalyticsPlugin = VisitorAnalyticsPlugin = VisitorAnalyticsPlugin_1 = __decorate([
|
|
88
|
+
(0, core_1.VendurePlugin)({
|
|
89
|
+
imports: [core_1.PluginCommonModule],
|
|
90
|
+
providers: [visitor_tracking_service_1.VisitorTrackingService],
|
|
91
|
+
controllers: [visitor_tracking_controller_1.VisitorTrackingController],
|
|
92
|
+
entities: [visitor_event_entity_1.VisitorEvent],
|
|
93
|
+
compatibility: '^3.0.0',
|
|
94
|
+
})
|
|
95
|
+
], VisitorAnalyticsPlugin);
|
|
96
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wCAAwE;AACxE,yEAAmF;AACnF,iEAAsD;AACtD,yEAAoE;AACpE,+EAA0E;AAY1E,MAAM,eAAe,GAAG;;;;;;;;yBAQC,CAAC;AAE1B,MAAM,SAAS,GAAG,kCAAkC,CAAC;AACrD,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B;OACvD,4CAA4C,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQI,IAAM,sBAAsB,8BAA5B,MAAM,sBAAsB;IAG/B,MAAM,CAAC,IAAI,CAAC,OAAsC;QAC9C,IAAI,CAAC,wBAAsB,CAAC,UAAU,EAAE,CAAC;YACrC,wBAAsB,CAAC,UAAU,GAAG,IAAI,uCAAiB,CAAC,cAAc,CAAC,CAAC;YAC1E,wBAAsB,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAC9C,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;aACrC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,IAAA,mCAAa,EAAC;YACzB,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,QAAQ,EAAE,SAAS;YACnB,IAAI;YACJ,SAAS,EAAE,eAAe;YAC1B,UAAU,EAAE,wBAAsB,CAAC,UAAU,CAAC,aAAa,EAAE;SAChE,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAChB,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACR,kDAAkD,MAAM,CAAC,OAAO,EAAE;gBAClE,uIAAuI,SAAS,EAAE,CACrJ,CAAC;QACN,CAAC;QAED,OAAO,wBAAsB,CAAC;IAClC,CAAC;;AA5BQ,wDAAsB;AAChB,iCAAU,GAA6B,IAAI,AAAjC,CAAkC;AA6BpD,mCAAY,GAAG;IAClB,aAAa,EAAE,SAAS,GAAG,QAAQ;IACnC,SAAS,EAAE;QACP;YACI,IAAI,EAAE,MAAe;YACrB,KAAK,EAAE,UAAU;YACjB,gBAAgB,EAAE,oBAAoB;YACtC,YAAY,EAAE,gBAAgB;SACjC;KACJ;CACJ,AAVkB,CAUjB;iCAxCO,sBAAsB;IAPlC,IAAA,oBAAa,EAAC;QACX,OAAO,EAAE,CAAC,yBAAkB,CAAC;QAC7B,SAAS,EAAE,CAAC,iDAAsB,CAAC;QACnC,WAAW,EAAE,CAAC,uDAAyB,CAAC;QACxC,QAAQ,EAAE,CAAC,mCAAY,CAAC;QACxB,aAAa,EAAE,QAAQ;KAC1B,CAAC;GACW,sBAAsB,CAyClC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Reverse-proxy aware visitor IP extraction.
|
|
4
|
+
*
|
|
5
|
+
* Order of precedence:
|
|
6
|
+
* 1. Cloudflare's `CF-Connecting-IP` (always the real client IP,
|
|
7
|
+
* regardless of how many proxies sit in front of the worker).
|
|
8
|
+
* 2. `True-Client-IP` (Akamai / Cloudflare Enterprise).
|
|
9
|
+
* 3. `X-Real-IP` (nginx / Caddy default when proxying).
|
|
10
|
+
* 4. First entry in `X-Forwarded-For` (RFC 7239 ancestor; the
|
|
11
|
+
* left-most entry is the original client when the upstream proxy
|
|
12
|
+
* is trusted).
|
|
13
|
+
* 5. Express's `req.ip` — only useful when `app.set('trust proxy', ...)`
|
|
14
|
+
* has been set on the Vendure host, otherwise this is the socket
|
|
15
|
+
* address of the last hop.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` if none of the headers are populated and `req.ip`
|
|
18
|
+
* isn't available — the caller should treat this as "unknown" and
|
|
19
|
+
* skip IP-dependent enrichment rather than fail.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getRealIp(req: Request): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Cloudflare / Akamai populate the visitor's resolved country on the
|
|
24
|
+
* inbound request when the corresponding feature is enabled. Reading
|
|
25
|
+
* the upstream value avoids a per-request GeoIP lookup. Returns the
|
|
26
|
+
* ISO 3166-1 alpha-2 country code or `null` if no proxy header is
|
|
27
|
+
* present.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getResolvedCountry(req: Request): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Cloudflare's `cf-region-code` carries the ISO 3166-2 subdivision
|
|
32
|
+
* (e.g. `ENG`, `SCT`, `CA`) when the "Send subdivision data" option is
|
|
33
|
+
* enabled in the dashboard. Returns the bare code without the country
|
|
34
|
+
* prefix, or `null` if unavailable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getResolvedRegion(req: Request): string | null;
|
|
37
|
+
//# sourceMappingURL=proxy-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-headers.d.ts","sourceRoot":"","sources":["../src/proxy-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkBrD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAe9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK7D"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRealIp = getRealIp;
|
|
4
|
+
exports.getResolvedCountry = getResolvedCountry;
|
|
5
|
+
exports.getResolvedRegion = getResolvedRegion;
|
|
6
|
+
/**
|
|
7
|
+
* Reverse-proxy aware visitor IP extraction.
|
|
8
|
+
*
|
|
9
|
+
* Order of precedence:
|
|
10
|
+
* 1. Cloudflare's `CF-Connecting-IP` (always the real client IP,
|
|
11
|
+
* regardless of how many proxies sit in front of the worker).
|
|
12
|
+
* 2. `True-Client-IP` (Akamai / Cloudflare Enterprise).
|
|
13
|
+
* 3. `X-Real-IP` (nginx / Caddy default when proxying).
|
|
14
|
+
* 4. First entry in `X-Forwarded-For` (RFC 7239 ancestor; the
|
|
15
|
+
* left-most entry is the original client when the upstream proxy
|
|
16
|
+
* is trusted).
|
|
17
|
+
* 5. Express's `req.ip` — only useful when `app.set('trust proxy', ...)`
|
|
18
|
+
* has been set on the Vendure host, otherwise this is the socket
|
|
19
|
+
* address of the last hop.
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` if none of the headers are populated and `req.ip`
|
|
22
|
+
* isn't available — the caller should treat this as "unknown" and
|
|
23
|
+
* skip IP-dependent enrichment rather than fail.
|
|
24
|
+
*/
|
|
25
|
+
function getRealIp(req) {
|
|
26
|
+
var _a;
|
|
27
|
+
const headers = req.headers || {};
|
|
28
|
+
const cfIp = String(headers['cf-connecting-ip'] || '').trim();
|
|
29
|
+
if (cfIp)
|
|
30
|
+
return cfIp;
|
|
31
|
+
const trueClient = String(headers['true-client-ip'] || '').trim();
|
|
32
|
+
if (trueClient)
|
|
33
|
+
return trueClient;
|
|
34
|
+
const realIp = String(headers['x-real-ip'] || '').trim();
|
|
35
|
+
if (realIp)
|
|
36
|
+
return realIp;
|
|
37
|
+
const xff = String(headers['x-forwarded-for'] || '').trim();
|
|
38
|
+
if (xff) {
|
|
39
|
+
const first = (_a = xff.split(',')[0]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
40
|
+
if (first)
|
|
41
|
+
return first;
|
|
42
|
+
}
|
|
43
|
+
return req.ip || null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Cloudflare / Akamai populate the visitor's resolved country on the
|
|
47
|
+
* inbound request when the corresponding feature is enabled. Reading
|
|
48
|
+
* the upstream value avoids a per-request GeoIP lookup. Returns the
|
|
49
|
+
* ISO 3166-1 alpha-2 country code or `null` if no proxy header is
|
|
50
|
+
* present.
|
|
51
|
+
*/
|
|
52
|
+
function getResolvedCountry(req) {
|
|
53
|
+
const headers = req.headers || {};
|
|
54
|
+
const cf = String(headers['cf-ipcountry'] || '').trim().toUpperCase();
|
|
55
|
+
if (cf && cf !== 'XX' && cf !== 'T1')
|
|
56
|
+
return cf;
|
|
57
|
+
const akamai = String(headers['x-akamai-edgescape'] || '').trim();
|
|
58
|
+
if (akamai) {
|
|
59
|
+
const m = akamai.match(/country_code=([A-Z]{2})/i);
|
|
60
|
+
if (m)
|
|
61
|
+
return m[1].toUpperCase();
|
|
62
|
+
}
|
|
63
|
+
const fastly = String(headers['x-country-code'] || '').trim().toUpperCase();
|
|
64
|
+
if (fastly && /^[A-Z]{2}$/.test(fastly))
|
|
65
|
+
return fastly;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cloudflare's `cf-region-code` carries the ISO 3166-2 subdivision
|
|
70
|
+
* (e.g. `ENG`, `SCT`, `CA`) when the "Send subdivision data" option is
|
|
71
|
+
* enabled in the dashboard. Returns the bare code without the country
|
|
72
|
+
* prefix, or `null` if unavailable.
|
|
73
|
+
*/
|
|
74
|
+
function getResolvedRegion(req) {
|
|
75
|
+
const headers = req.headers || {};
|
|
76
|
+
const cf = String(headers['cf-region-code'] || '').trim().toUpperCase();
|
|
77
|
+
if (cf && /^[A-Z0-9]{1,4}$/.test(cf))
|
|
78
|
+
return cf;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=proxy-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-headers.js","sourceRoot":"","sources":["../src/proxy-headers.ts"],"names":[],"mappings":";;AAqBA,8BAkBC;AASD,gDAeC;AAQD,8CAKC;AA1ED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,SAAS,CAAC,GAAY;;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9D,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAEtB,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACzD,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,IAAI,GAAG,EAAE,CAAC;QACN,MAAM,KAAK,GAAG,MAAA,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,0CAAE,IAAI,EAAE,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC5B,CAAC;IAED,OAAQ,GAAW,CAAC,EAAE,IAAI,IAAI,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,kBAAkB,CAAC,GAAY;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;IAClC,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtE,IAAI,EAAE,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAEhD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,IAAI,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QACnD,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5E,IAAI,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAEvD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,GAAY;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;IAClC,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACxE,IAAI,EAAE,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAChD,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DeepPartial, VendureEntity } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* Stores a single visitor analytics event — used to map the customer
|
|
4
|
+
* journey across the storefront, including for visitors who never log in.
|
|
5
|
+
*
|
|
6
|
+
* type='pageview' — a new page was opened (one row per navigation)
|
|
7
|
+
* type='unload' — a page was closed / hidden; `timeOnPageMs` is the
|
|
8
|
+
* time the visitor spent on it before leaving
|
|
9
|
+
* type='event' — a custom event raised by the frontend (e.g. an
|
|
10
|
+
* add-to-cart click); the payload lives in `meta`
|
|
11
|
+
*
|
|
12
|
+
* `visitorId` is a long-lived cookie UUID (2 years) — identifies the
|
|
13
|
+
* device across sessions. `sessionId` is a short-lived cookie UUID
|
|
14
|
+
* (30-minute idle expiry) — identifies a single browsing burst.
|
|
15
|
+
*
|
|
16
|
+
* `customerId` is set when the visitor is logged in; the same
|
|
17
|
+
* `visitorId` can carry guest events first and then customer events
|
|
18
|
+
* once the visitor signs in / signs up — that's what makes the funnel
|
|
19
|
+
* analysis work.
|
|
20
|
+
*/
|
|
21
|
+
export declare class VisitorEvent extends VendureEntity {
|
|
22
|
+
constructor(input?: DeepPartial<VisitorEvent>);
|
|
23
|
+
visitorId: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
customerId: number | null;
|
|
26
|
+
channelId: number;
|
|
27
|
+
type: 'pageview' | 'unload' | 'event' | string;
|
|
28
|
+
/** Pathname + search, no host. Capped at 2048 to fit Cloudflare's max URL. */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Document title at the time of the event — useful for the journey
|
|
31
|
+
* timeline so the admin sees "Product: Windows 11" instead of just
|
|
32
|
+
* "/products/microsoft-windows-11-professional/". */
|
|
33
|
+
title: string | null;
|
|
34
|
+
referrer: string | null;
|
|
35
|
+
/** Set only on `unload` rows — milliseconds the visitor spent on the
|
|
36
|
+
* page before leaving. */
|
|
37
|
+
timeOnPageMs: number | null;
|
|
38
|
+
/** Hash of the visitor's IP address — kept for spot-the-same-IP-bot
|
|
39
|
+
* analysis even after the raw IP is purged. */
|
|
40
|
+
ipHash: string | null;
|
|
41
|
+
/** Raw client IP. Max length 45 chars covers full IPv6 + zone id. */
|
|
42
|
+
ip: string | null;
|
|
43
|
+
userAgent: string | null;
|
|
44
|
+
/** Parsed user-agent — populated server-side at ingest. */
|
|
45
|
+
browser: string | null;
|
|
46
|
+
browserVersion: string | null;
|
|
47
|
+
os: string | null;
|
|
48
|
+
osVersion: string | null;
|
|
49
|
+
device: string | null;
|
|
50
|
+
/** `Accept-Language` header, capped. */
|
|
51
|
+
acceptLanguage: string | null;
|
|
52
|
+
country: string | null;
|
|
53
|
+
region: string | null;
|
|
54
|
+
city: string | null;
|
|
55
|
+
timezone: string | null;
|
|
56
|
+
/** Arbitrary JSON payload for `type='event'` rows. */
|
|
57
|
+
meta: string | null;
|
|
58
|
+
utmSource: string | null;
|
|
59
|
+
utmMedium: string | null;
|
|
60
|
+
utmCampaign: string | null;
|
|
61
|
+
utmTerm: string | null;
|
|
62
|
+
utmContent: string | null;
|
|
63
|
+
/** Lowercased host of `referrer` (e.g. `google.com`, `facebook.com`).
|
|
64
|
+
* Stored alongside the full referrer string so admin reports can
|
|
65
|
+
* group by domain without parsing every URL at read time. */
|
|
66
|
+
referrerDomain: string | null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=visitor-event.entity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visitor-event.entity.d.ts","sourceRoot":"","sources":["../src/visitor-event.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAMa,YAAa,SAAQ,aAAa;gBAC/B,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC;IAG7C,SAAS,EAAG,MAAM,CAAC;IAGnB,SAAS,EAAG,MAAM,CAAC;IAGnB,UAAU,EAAG,MAAM,GAAG,IAAI,CAAC;IAG3B,SAAS,EAAG,MAAM,CAAC;IAGnB,IAAI,EAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IAEhD,8EAA8E;IAE9E,GAAG,EAAG,MAAM,CAAC;IAEb;;0DAEsD;IAEtD,KAAK,EAAG,MAAM,GAAG,IAAI,CAAC;IAGtB,QAAQ,EAAG,MAAM,GAAG,IAAI,CAAC;IAEzB;+BAC2B;IAE3B,YAAY,EAAG,MAAM,GAAG,IAAI,CAAC;IAE7B;oDACgD;IAEhD,MAAM,EAAG,MAAM,GAAG,IAAI,CAAC;IAEvB,qEAAqE;IAErE,EAAE,EAAG,MAAM,GAAG,IAAI,CAAC;IAGnB,SAAS,EAAG,MAAM,GAAG,IAAI,CAAC;IAE1B,2DAA2D;IAE3D,OAAO,EAAG,MAAM,GAAG,IAAI,CAAC;IAGxB,cAAc,EAAG,MAAM,GAAG,IAAI,CAAC;IAG/B,EAAE,EAAG,MAAM,GAAG,IAAI,CAAC;IAGnB,SAAS,EAAG,MAAM,GAAG,IAAI,CAAC;IAG1B,MAAM,EAAG,MAAM,GAAG,IAAI,CAAC;IAEvB,wCAAwC;IAExC,cAAc,EAAG,MAAM,GAAG,IAAI,CAAC;IAG/B,OAAO,EAAG,MAAM,GAAG,IAAI,CAAC;IAGxB,MAAM,EAAG,MAAM,GAAG,IAAI,CAAC;IAGvB,IAAI,EAAG,MAAM,GAAG,IAAI,CAAC;IAGrB,QAAQ,EAAG,MAAM,GAAG,IAAI,CAAC;IAEzB,sDAAsD;IAEtD,IAAI,EAAG,MAAM,GAAG,IAAI,CAAC;IAUrB,SAAS,EAAG,MAAM,GAAG,IAAI,CAAC;IAG1B,SAAS,EAAG,MAAM,GAAG,IAAI,CAAC;IAG1B,WAAW,EAAG,MAAM,GAAG,IAAI,CAAC;IAG5B,OAAO,EAAG,MAAM,GAAG,IAAI,CAAC;IAGxB,UAAU,EAAG,MAAM,GAAG,IAAI,CAAC;IAE3B;;kEAE8D;IAE9D,cAAc,EAAG,MAAM,GAAG,IAAI,CAAC;CAClC"}
|