@ht-sdks/events-sdk-js-browser 1.2.0 → 1.3.1
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 +117 -4
- package/dist/cjs/browser/index.js +57 -31
- package/dist/cjs/browser/index.js.map +1 -1
- package/dist/cjs/core/analytics/index.js.map +1 -1
- package/dist/cjs/core/http-cookies/index.js +11 -3
- package/dist/cjs/core/http-cookies/index.js.map +1 -1
- package/dist/cjs/core/user/index.js +7 -1
- package/dist/cjs/core/user/index.js.map +1 -1
- package/dist/cjs/core/user/tld.js +7 -3
- package/dist/cjs/core/user/tld.js.map +1 -1
- package/dist/cjs/generated/version.js +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/plugins/destinations/destination.js +81 -0
- package/dist/cjs/plugins/destinations/destination.js.map +1 -0
- package/dist/cjs/plugins/destinations/google-tag-manager.js +34 -0
- package/dist/cjs/plugins/destinations/google-tag-manager.js.map +1 -0
- package/dist/cjs/plugins/destinations/gtag.js +44 -0
- package/dist/cjs/plugins/destinations/gtag.js.map +1 -0
- package/dist/cjs/plugins/destinations/index.js +26 -0
- package/dist/cjs/plugins/destinations/index.js.map +1 -0
- package/dist/cjs/plugins/destinations/types.js +3 -0
- package/dist/cjs/plugins/destinations/types.js.map +1 -0
- package/dist/pkg/browser/index.js +57 -31
- package/dist/pkg/browser/index.js.map +1 -1
- package/dist/pkg/core/analytics/index.js.map +1 -1
- package/dist/pkg/core/http-cookies/index.js +11 -3
- package/dist/pkg/core/http-cookies/index.js.map +1 -1
- package/dist/pkg/core/user/index.js +7 -1
- package/dist/pkg/core/user/index.js.map +1 -1
- package/dist/pkg/core/user/tld.js +7 -3
- package/dist/pkg/core/user/tld.js.map +1 -1
- package/dist/pkg/generated/version.js +1 -1
- package/dist/pkg/index.js +1 -0
- package/dist/pkg/index.js.map +1 -1
- package/dist/pkg/plugins/destinations/destination.js +78 -0
- package/dist/pkg/plugins/destinations/destination.js.map +1 -0
- package/dist/pkg/plugins/destinations/google-tag-manager.js +32 -0
- package/dist/pkg/plugins/destinations/google-tag-manager.js.map +1 -0
- package/dist/pkg/plugins/destinations/gtag.js +42 -0
- package/dist/pkg/plugins/destinations/gtag.js.map +1 -0
- package/dist/pkg/plugins/destinations/index.js +22 -0
- package/dist/pkg/plugins/destinations/index.js.map +1 -0
- package/dist/pkg/plugins/destinations/types.js +2 -0
- package/dist/pkg/plugins/destinations/types.js.map +1 -0
- package/dist/types/browser/index.d.ts.map +1 -1
- package/dist/types/core/analytics/index.d.ts +5 -0
- package/dist/types/core/analytics/index.d.ts.map +1 -1
- package/dist/types/core/buffer/index.d.ts +1 -1
- package/dist/types/core/http-cookies/index.d.ts +4 -0
- package/dist/types/core/http-cookies/index.d.ts.map +1 -1
- package/dist/types/core/user/index.d.ts.map +1 -1
- package/dist/types/core/user/tld.d.ts.map +1 -1
- package/dist/types/generated/version.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/plugins/destinations/destination.d.ts +32 -0
- package/dist/types/plugins/destinations/destination.d.ts.map +1 -0
- package/dist/types/plugins/destinations/google-tag-manager.d.ts +26 -0
- package/dist/types/plugins/destinations/google-tag-manager.d.ts.map +1 -0
- package/dist/types/plugins/destinations/gtag.d.ts +27 -0
- package/dist/types/plugins/destinations/gtag.d.ts.map +1 -0
- package/dist/types/plugins/destinations/index.d.ts +6 -0
- package/dist/types/plugins/destinations/index.d.ts.map +1 -0
- package/dist/types/plugins/destinations/types.d.ts +5 -0
- package/dist/types/plugins/destinations/types.d.ts.map +1 -0
- package/dist/umd/events.min.js +1 -1
- package/dist/umd/events.min.js.map +1 -1
- package/dist/umd/google-tag-manager.bundle.238ad1d40bdf04d984ef.js +2 -0
- package/dist/umd/google-tag-manager.bundle.238ad1d40bdf04d984ef.js.map +1 -0
- package/dist/umd/gtag.bundle.a5e7c472f1c04f2b9ebd.js +2 -0
- package/dist/umd/gtag.bundle.a5e7c472f1c04f2b9ebd.js.map +1 -0
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +13 -18
- package/src/browser/index.ts +14 -1
- package/src/core/analytics/index.ts +6 -0
- package/src/core/http-cookies/README.md +9 -106
- package/src/core/http-cookies/index.ts +16 -3
- package/src/core/http-cookies/server-examples/node-aws-lambda.md +167 -0
- package/src/core/http-cookies/server-examples/node-express-js.md +103 -0
- package/src/core/http-cookies/server-examples/node-next-js.md +75 -0
- package/src/core/user/index.ts +8 -0
- package/src/core/user/tld.ts +8 -4
- package/src/generated/version.ts +1 -1
- package/src/index.ts +1 -0
- package/src/plugins/destinations/destination.ts +85 -0
- package/src/plugins/destinations/google-tag-manager.ts +75 -0
- package/src/plugins/destinations/gtag.ts +93 -0
- package/src/plugins/destinations/index.ts +23 -0
- package/src/plugins/destinations/types.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ht-sdks/events-sdk-js-browser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/ht-sdks/events-sdk-js-mono",
|
|
@@ -24,28 +24,24 @@
|
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"scripts": {
|
|
27
|
-
".": "yarn run -T turbo run --filter=@ht-sdks/events-sdk-js-browser...",
|
|
28
27
|
"build-prep": "sh scripts/build-prep.sh",
|
|
29
|
-
"version": "
|
|
28
|
+
"version": "npm run build-prep && git add src/generated/version.ts",
|
|
30
29
|
"umd": "webpack",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"candidate:cdn": "yarn . build && NODE_ENV=production PATH_PREFIX=browser/candidate bash scripts/release.sh",
|
|
38
|
-
"release:cdn": "yarn . build && NODE_ENV=production PATH_PREFIX=browser/release bash scripts/release.sh",
|
|
39
|
-
"pkg": "yarn tsc -p tsconfig.build.json",
|
|
40
|
-
"cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs",
|
|
30
|
+
"watch": "concurrently 'NODE_ENV=production WATCH=true npm run umd -- --watch' 'npm run pkg -- --watch'",
|
|
31
|
+
"build": "npm run clean && npm run build-prep && concurrently 'NODE_ENV=production npm run umd' 'npm:pkg' 'npm:cjs'",
|
|
32
|
+
"candidate:cdn": "turbo run build && NODE_ENV=production PATH_PREFIX=browser/candidate bash scripts/release.sh",
|
|
33
|
+
"release:cdn": "turbo run build && NODE_ENV=production PATH_PREFIX=browser/release bash scripts/release.sh",
|
|
34
|
+
"pkg": "tsc -p tsconfig.build.json",
|
|
35
|
+
"cjs": "tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs",
|
|
41
36
|
"clean": "rm -rf dist",
|
|
42
|
-
"lint": "
|
|
43
|
-
"test": "
|
|
37
|
+
"lint": "concurrently 'eslint .' 'tsc --noEmit'",
|
|
38
|
+
"test": "jest",
|
|
39
|
+
"size-limit": "size-limit"
|
|
44
40
|
},
|
|
45
41
|
"size-limit": [
|
|
46
42
|
{
|
|
47
43
|
"path": "dist/umd/index.js",
|
|
48
|
-
"limit": "
|
|
44
|
+
"limit": "37 KB"
|
|
49
45
|
}
|
|
50
46
|
],
|
|
51
47
|
"dependencies": {
|
|
@@ -101,6 +97,5 @@
|
|
|
101
97
|
"webpack": "^5.76.0",
|
|
102
98
|
"webpack-bundle-analyzer": "^4.4.2",
|
|
103
99
|
"webpack-cli": "^4.8.0"
|
|
104
|
-
}
|
|
105
|
-
"packageManager": "yarn@3.4.1"
|
|
100
|
+
}
|
|
106
101
|
}
|
package/src/browser/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { ClassicIntegrationSource } from '../plugins/ajs-destination/types'
|
|
31
31
|
import { attachInspector } from '../core/inspector'
|
|
32
32
|
import { setGlobalAnalyticsKey } from '../lib/global-analytics-helper'
|
|
33
|
+
import { createDestination } from '../plugins/destinations'
|
|
33
34
|
|
|
34
35
|
export interface LegacyIntegrationConfiguration {
|
|
35
36
|
/* @deprecated - This does not indicate browser types anymore */
|
|
@@ -270,6 +271,18 @@ async function registerPlugins(
|
|
|
270
271
|
)
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
// destination plugins
|
|
275
|
+
await Promise.allSettled(
|
|
276
|
+
Object.entries(options.destinations ?? {}).map(async ([name, settings]) => {
|
|
277
|
+
const plugin = await createDestination(name, settings)
|
|
278
|
+
if (plugin) {
|
|
279
|
+
toRegister.push(plugin)
|
|
280
|
+
} else {
|
|
281
|
+
console.warn(`failed to load destination plugin: ${name}`)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
|
|
273
286
|
const ctx = await analytics.register(...toRegister)
|
|
274
287
|
|
|
275
288
|
if (
|
|
@@ -363,7 +376,7 @@ async function loadAnalytics(
|
|
|
363
376
|
if (!options.disableClientPersistence && options.httpCookieServiceOptions) {
|
|
364
377
|
options.httpCookieService = await HTTPCookieService.load(
|
|
365
378
|
options.httpCookieServiceOptions
|
|
366
|
-
)
|
|
379
|
+
).catch((err): undefined => console.error(err) as undefined)
|
|
367
380
|
}
|
|
368
381
|
|
|
369
382
|
const opts: InitOptions = { retryQueue, ...options }
|
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
HTTPCookieService,
|
|
60
60
|
HTTPCookieServiceOptions,
|
|
61
61
|
} from '../http-cookies'
|
|
62
|
+
import type { DestinationSettings } from '../../plugins/destinations'
|
|
62
63
|
|
|
63
64
|
const deprecationWarning =
|
|
64
65
|
'This is being deprecated and will be not be available in future releases of Analytics JS'
|
|
@@ -133,6 +134,11 @@ export interface InitOptions {
|
|
|
133
134
|
*/
|
|
134
135
|
globalAnalyticsKey?: string
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Allows specifying plugins as configuration. Used to load plugins in `plugins/destinations/*`.
|
|
139
|
+
*/
|
|
140
|
+
destinations?: Record<string, DestinationSettings>
|
|
141
|
+
|
|
136
142
|
/**
|
|
137
143
|
* When setting httpCookieServiceOptions, an HTTPCookieService is automatically created
|
|
138
144
|
*/
|
|
@@ -45,8 +45,8 @@ const htevents = HtEventsBrowser.load(
|
|
|
45
45
|
{
|
|
46
46
|
apiHost: "us-east-1.hightouch-events.com", // HtEvents API remains the same
|
|
47
47
|
httpCookieServiceOptions: {
|
|
48
|
-
clearUrl: 'ht/clear', // route hosted on *your* domain and infra
|
|
49
|
-
renewUrl: 'ht/renew', // route hosted on *your* domain and infra
|
|
48
|
+
clearUrl: '/ht/clear', // route hosted on *your* domain and infra
|
|
49
|
+
renewUrl: '/ht/renew', // route hosted on *your* domain and infra
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
)
|
|
@@ -79,116 +79,19 @@ If there are no browser cookies found, return any server cookies as browser cook
|
|
|
79
79
|
### An API for **clearing** server cookies
|
|
80
80
|
|
|
81
81
|
This route should look for **server** cookies and clean them:
|
|
82
|
-
* `res.cookie("htjs_anonymous_id_srvr", "", {maxAge: 0, httpOnly:true});`
|
|
83
|
-
* `res.cookie("htjs_user_id_srvr", "", {maxAge: 0, httpOnly:true});`
|
|
82
|
+
* `res.cookie("htjs_anonymous_id_srvr", "", {maxAge: 0, httpOnly:true, ...});`
|
|
83
|
+
* `res.cookie("htjs_user_id_srvr", "", {maxAge: 0, httpOnly:true, ...});`
|
|
84
84
|
|
|
85
85
|
### API Spec
|
|
86
86
|
The spec of the actual `request` and `response` payloads are kept intentionally vague. The spec should fit a variety of server environments.
|
|
87
87
|
|
|
88
|
-
The Events SDK only requires that the server: A) handles cookies and B) returns a `200`
|
|
89
|
-
|
|
90
|
-
## Server Example
|
|
91
|
-
|
|
92
|
-
A simplified Node.js/Express server:
|
|
93
|
-
|
|
94
|
-
```Javascript
|
|
95
|
-
const express = require("express");
|
|
96
|
-
const cookieParser = require('cookie-parser');
|
|
97
|
-
const cors = require('cors');
|
|
98
|
-
|
|
99
|
-
const USER_COOKIE = "htjs_user_id";
|
|
100
|
-
const ANON_COOKIE = "htjs_anonymous_id";
|
|
101
|
-
|
|
102
|
-
function getDomain(req) {
|
|
103
|
-
let domain = process.env.DOMAIN || req.headers["x-forwarded-for"] || req.get("host");
|
|
104
|
-
if (domain.startsWith("localhost")) return "localhost";
|
|
105
|
-
return domain;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function renewCookies(req, res, browserName, serverName) {
|
|
109
|
-
const cookie = req.cookies[browserName] || req.cookies[serverName];
|
|
110
|
-
if (!cookie) return "";
|
|
111
|
-
const cookieParams = {maxAge:31536000*1000, domain: getDomain(req), sameSite: "lax"};
|
|
112
|
-
res.cookie(browserName, cookie, {...cookieParams});
|
|
113
|
-
res.cookie(serverName, cookie, {...cookieParams, httpOnly:true});
|
|
114
|
-
return cookie;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function clearServerCookie(req, res, serverName) {
|
|
118
|
-
const cookie = "";
|
|
119
|
-
const cookieParams = {maxAge:0, domain: getDomain(req), sameSite: "lax"};
|
|
120
|
-
res.cookie(serverName, "", {...cookieParams, httpOnly:true});
|
|
121
|
-
return cookie;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const app = express();
|
|
125
|
-
app.use(cookieParser());
|
|
126
|
-
app.use(cors())
|
|
127
|
-
|
|
128
|
-
app.post("/ht/renew", (req, res) => {
|
|
129
|
-
// recreate a browser cookie from an existing server cookie, OR
|
|
130
|
-
// create a new server cookie that can later be used to recreate from.
|
|
131
|
-
return res.json({
|
|
132
|
-
userId: renewCookies(req, res, USER_COOKIE, `${USER_COOKIE}_srvr`),
|
|
133
|
-
anonymousId: renewCookies(req, res, ANON_COOKIE, `${ANON_COOKIE}_srvr`),
|
|
134
|
-
})
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
app.post("/ht/clear", (req, res) => {
|
|
138
|
-
// clear server cookies, e.g. if the user asks to clear all cookies.
|
|
139
|
-
return res.json({
|
|
140
|
-
userId: clearServerCookie(req, res, `${USER_COOKIE}_srvr`),
|
|
141
|
-
anonymousId: clearServerCookie(req, res, `${ANON_COOKIE}_srvr`),
|
|
142
|
-
})
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
app.listen(3000, () => {
|
|
146
|
-
console.log("Listening on port 3000...");
|
|
147
|
-
});
|
|
148
|
-
```
|
|
88
|
+
The Events SDK only requires that the server: A) handles cookies and B) returns a `200` status code.
|
|
149
89
|
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
worker_processes 1;
|
|
153
|
-
|
|
154
|
-
events {
|
|
155
|
-
worker_connections 1024;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
http {
|
|
159
|
-
default_type application/octet-stream;
|
|
160
|
-
sendfile on;
|
|
161
|
-
keepalive_timeout 65;
|
|
162
|
-
|
|
163
|
-
server {
|
|
164
|
-
listen 8080;
|
|
165
|
-
server_name localhost;
|
|
166
|
-
|
|
167
|
-
location / {
|
|
168
|
-
root /Users/name/src/website/html;
|
|
169
|
-
index index.html index.htm;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
location /cdn {
|
|
173
|
-
#autoindex on;
|
|
174
|
-
alias /Users/name/src/website/cdn;
|
|
175
|
-
try_files $uri /index.html =404;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
location /ht {
|
|
179
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
180
|
-
proxy_set_header Host $host;
|
|
181
|
-
proxy_pass http://127.0.0.1:3000;
|
|
182
|
-
}
|
|
90
|
+
## Server Examples
|
|
183
91
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**These server examples should not be used as is.** They should be adapted to your setup and "productionized". The general concepts remain the same though.
|
|
92
|
+
- [Express.js and NGINX](./server-examples/node-express-js.md)
|
|
93
|
+
- [Next.js and Vercel](./server-examples/node-next-js.md)
|
|
94
|
+
- [AWS Lambda and API Gateway](./server-examples/node-aws-lambda.md)
|
|
191
95
|
|
|
192
96
|
## More information
|
|
193
97
|
- Safari: https://webkit.org/blog/9521/intelligent-tracking-prevention-2-3/
|
|
194
|
-
|
|
@@ -35,14 +35,27 @@ export class HTTPCookieService {
|
|
|
35
35
|
private flushIntervalId?: NodeJS.Timer
|
|
36
36
|
|
|
37
37
|
private constructor(options: HTTPCookieServiceOptions) {
|
|
38
|
-
|
|
39
|
-
this.
|
|
38
|
+
const urls = HTTPCookieService.urlHelper(options)
|
|
39
|
+
this.renewUrl = urls.renewUrl
|
|
40
|
+
this.clearUrl = urls.clearUrl
|
|
41
|
+
|
|
40
42
|
this.backoff = options.backoff ?? 300
|
|
41
43
|
this.retries = options.retries ?? 3
|
|
42
44
|
this.flushInterval = options.flushInterval ?? 1000
|
|
43
45
|
this.queue = []
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
static urlHelper(options: HTTPCookieServiceOptions): {
|
|
49
|
+
renewUrl: string
|
|
50
|
+
clearUrl: string
|
|
51
|
+
} {
|
|
52
|
+
const origin = window.location.origin
|
|
53
|
+
return {
|
|
54
|
+
renewUrl: new URL(options.renewUrl, origin).href,
|
|
55
|
+
clearUrl: new URL(options.clearUrl, origin).href,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
static async load(
|
|
47
60
|
options: HTTPCookieServiceOptions
|
|
48
61
|
): Promise<HTTPCookieService> {
|
|
@@ -50,7 +63,7 @@ export class HTTPCookieService {
|
|
|
50
63
|
|
|
51
64
|
// renew any existing HTTPCookies already on the device
|
|
52
65
|
// we want `load()` to block on this, so await directly instead of calling dispatch
|
|
53
|
-
const req = cookieService.sendHTTPCookies(
|
|
66
|
+
const req = cookieService.sendHTTPCookies(cookieService.renewUrl)
|
|
54
67
|
await retry(req, cookieService.retries, cookieService.backoff).catch(
|
|
55
68
|
console.error
|
|
56
69
|
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
**These server examples should not be used as is. They should be adapted to your setup and "productionized".**
|
|
2
|
+
|
|
3
|
+
An example HTTPCookieService written as an AWS Lambda Function:
|
|
4
|
+
|
|
5
|
+
```Javascript
|
|
6
|
+
const USER_COOKIE = "htjs_user_id";
|
|
7
|
+
const ANON_COOKIE = "htjs_anonymous_id";
|
|
8
|
+
const DOMAIN = "CHANGEME.example.com";
|
|
9
|
+
|
|
10
|
+
function renewCookies(request, response, browserName, serverName) {
|
|
11
|
+
let cookie = request.cookies[browserName] ?? request.cookies[serverName];
|
|
12
|
+
if (!cookie) return "";
|
|
13
|
+
const maxAge = 31_536_000; // 1 year in seconds
|
|
14
|
+
response.cookies= (response.cookies ?? []).concat([
|
|
15
|
+
`${browserName}=${cookie}; Max-Age=${maxAge}; Domain=${DOMAIN}; Path=/; SameSite=Lax;`,
|
|
16
|
+
`${serverName}=${cookie}; Max-Age=${maxAge}; Domain=${DOMAIN}; Path=/; SameSite=Lax; httpOnly=true;`,
|
|
17
|
+
]);
|
|
18
|
+
return cookie;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clearServerCookie(request, response, serverName) {
|
|
22
|
+
const cookie = "";
|
|
23
|
+
const maxAge = 0;
|
|
24
|
+
response.cookies = (response.cookies ?? []).concat([
|
|
25
|
+
`${serverName}=${cookie}; Max-Age=${maxAge}; Domain=${DOMAIN}; Path=/; SameSite=Lax; httpOnly;`,
|
|
26
|
+
]);
|
|
27
|
+
return cookie;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getCookies(reqCookies) {
|
|
31
|
+
const cookies = {};
|
|
32
|
+
if (!reqCookies) return cookies;
|
|
33
|
+
for (const cookieStr of reqCookies) {
|
|
34
|
+
const cookieArr = cookieStr.split("=");
|
|
35
|
+
if (cookieArr.length == 1) {
|
|
36
|
+
cookies[cookieArr[0]] = "";
|
|
37
|
+
} else if (cookieArr.length == 2) {
|
|
38
|
+
cookies[cookieArr[0]] = cookieArr[1];
|
|
39
|
+
} else {
|
|
40
|
+
console.log("cookieArr", cookieArr);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return cookies;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const handler = async (event, context) => {
|
|
47
|
+
const request = {
|
|
48
|
+
method: event.requestContext.http.method,
|
|
49
|
+
path: event.pathParameters.proxy,
|
|
50
|
+
headers: event.headers,
|
|
51
|
+
cookies: getCookies(event.cookies),
|
|
52
|
+
}
|
|
53
|
+
const response = {
|
|
54
|
+
statusCode: '200',
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json"
|
|
57
|
+
},
|
|
58
|
+
isBase64Encoded: false,
|
|
59
|
+
multiValueHeaders: {},
|
|
60
|
+
body: "{}",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (request.method?.toUpperCase() !== "POST") {
|
|
64
|
+
response.statusCode = 404;
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (request.path === "renew") {
|
|
69
|
+
const userId = renewCookies(request, response, USER_COOKIE, `${USER_COOKIE}_srvr`);
|
|
70
|
+
const anonymousId = renewCookies(request, response, ANON_COOKIE, `${ANON_COOKIE}_srvr`);
|
|
71
|
+
response.body = JSON.stringify({
|
|
72
|
+
userId,
|
|
73
|
+
anonymousId,
|
|
74
|
+
})
|
|
75
|
+
} else if (request.path === "clear") {
|
|
76
|
+
const userId = clearServerCookie(request, response, `${USER_COOKIE}_srvr`);
|
|
77
|
+
const anonymousId = clearServerCookie(request, response, `${ANON_COOKIE}_srvr`);
|
|
78
|
+
response.body = JSON.stringify({
|
|
79
|
+
userId,
|
|
80
|
+
anonymousId,
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
response.statusCode = 404;
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
After creating the above Lambda Function, you'll need to setup an [API Gateway](https://aws.amazon.com/api-gateway/). Specifically, create a new `Route` and input the value of `/ht/{proxy}`. Then attach an `integration` for this route, and select your Lambda. Be sure to select the "version 2" payload format. Your HTTPCookieService lambda will now be reachable via HTTP at `${ApiGatewayURL}.com/default/ht/renew`.
|
|
90
|
+
|
|
91
|
+
However, the HTTPCookieService must live on the same domain and IP address as your website's HTML document.
|
|
92
|
+
|
|
93
|
+
As one way to accomplish this, you could use a CDN to front both your HTML document and your API Gateway. To do this, create a Cloudfront Distribution. Then create multiple origin configurations. One will have a `customOriginSource`, and a `pathPattern` of `/ht/*` pointing at your API Gateway's URL. The other origin configuration will point at an `s3OriginSource` if your HTML website is being hosted on S3. You would then configure your Web SDK to point at `/ht/renew` and `/ht/clear` for the HTTPCookieService routes.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
Alternatively, if you simply want a "Hello World" example for a fully functioning HTTPCookieService, you can create two lambdas (one for HTML and another for HTTPCookieService), and put them both on the same API Gateway. You can then test against the API Gateway domain.
|
|
98
|
+
|
|
99
|
+
Example "Lambda for HTML". This is only intended for testing and "Hello World" purposes:
|
|
100
|
+
|
|
101
|
+
```Javascript
|
|
102
|
+
const cdnDomain = "https://cdn.hightouch-events.com/browser/release/v1-latest/events.min.js";
|
|
103
|
+
|
|
104
|
+
const writeKey = "WRITE KEY";
|
|
105
|
+
|
|
106
|
+
const html = `
|
|
107
|
+
<head>
|
|
108
|
+
<script type="text/javascript">
|
|
109
|
+
!function(){var e=window.htevents=window.htevents||[];if(!e.initialize)if(e.invoked)window.console&&console.error&&console.error("Hightouch snippet included twice.");else{e.invoked=!0,e.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"],e.factory=function(t){return function(){var n=Array.prototype.slice.call(arguments);return n.unshift(t),e.push(n),e}};for(var t=0;t<e.methods.length;t++){var n=e.methods[t];e[n]=e.factory(n)}e.load=function(t,n){var o=document.createElement("script");o.type="text/javascript",o.async=!0,o.src="${cdnDomain}";var r=document.getElementsByTagName("script")[0];r.parentNode.insertBefore(o,r),e._loadOptions=n,e._writeKey=t},e.SNIPPET_VERSION="0.0.1",
|
|
110
|
+
e.load('${writeKey}',{
|
|
111
|
+
apiHost:'us-east-1.hightouch-events.com',
|
|
112
|
+
httpCookieServiceOptions: {clearUrl: 'default/ht/clear', renewUrl: 'default/ht/renew', backoff: 5000},
|
|
113
|
+
}),
|
|
114
|
+
e.page()}}();
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
Hightouch
|
|
120
|
+
<a href="#" onClick="(function(){
|
|
121
|
+
htevents.identify(
|
|
122
|
+
'123', {
|
|
123
|
+
email: 'bob@hightouch.io'
|
|
124
|
+
}, {},
|
|
125
|
+
() => {
|
|
126
|
+
console.log('identify call');
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
})();return false;">identify
|
|
130
|
+
</a>
|
|
131
|
+
<br>
|
|
132
|
+
<br>
|
|
133
|
+
<a href="#" onClick="(function(){
|
|
134
|
+
htevents.track(
|
|
135
|
+
'clickEvent', {
|
|
136
|
+
revenue: 30,
|
|
137
|
+
currency: 'USD',
|
|
138
|
+
user_actual_id: 123
|
|
139
|
+
}, {},
|
|
140
|
+
() => {
|
|
141
|
+
console.log('track call');
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
})();return false;">track
|
|
145
|
+
</a>
|
|
146
|
+
<br>
|
|
147
|
+
<br>
|
|
148
|
+
<a href="#" onClick="(function(){
|
|
149
|
+
htevents.reset();
|
|
150
|
+
htevents.identify('456', { email: 'george@hightouch.com'})
|
|
151
|
+
})();return false;">reset
|
|
152
|
+
</a>
|
|
153
|
+
</body>
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
export const handler = async () => {
|
|
157
|
+
const response = {
|
|
158
|
+
statusCode: 200,
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'text/html',
|
|
161
|
+
},
|
|
162
|
+
body: html,
|
|
163
|
+
};
|
|
164
|
+
return response;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
**These server examples should not be used as is. They should be adapted to your setup and "productionized".**
|
|
2
|
+
|
|
3
|
+
An example HTTPCookieService written in Node.js/Express.js:
|
|
4
|
+
|
|
5
|
+
```Javascript
|
|
6
|
+
const express = require("express");
|
|
7
|
+
const cookieParser = require('cookie-parser');
|
|
8
|
+
const cors = require('cors');
|
|
9
|
+
|
|
10
|
+
const USER_COOKIE = "htjs_user_id";
|
|
11
|
+
const ANON_COOKIE = "htjs_anonymous_id";
|
|
12
|
+
|
|
13
|
+
function getDomain(req) {
|
|
14
|
+
let domain = process.env.DOMAIN || req.headers["x-forwarded-for"] || req.get("host");
|
|
15
|
+
if (domain.startsWith("localhost")) return "localhost";
|
|
16
|
+
return domain;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renewCookies(req, res, browserName, serverName) {
|
|
20
|
+
const cookie = req.cookies[browserName] || req.cookies[serverName];
|
|
21
|
+
if (!cookie) return "";
|
|
22
|
+
const cookieParams = {maxAge:31536000*1000, domain: getDomain(req), sameSite: "lax"};
|
|
23
|
+
res.cookie(browserName, cookie, {...cookieParams});
|
|
24
|
+
res.cookie(serverName, cookie, {...cookieParams, httpOnly:true});
|
|
25
|
+
return cookie;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearServerCookie(req, res, serverName) {
|
|
29
|
+
const cookie = "";
|
|
30
|
+
const cookieParams = {maxAge:0, domain: getDomain(req), sameSite: "lax"};
|
|
31
|
+
res.cookie(serverName, "", {...cookieParams, httpOnly:true});
|
|
32
|
+
return cookie;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use(cookieParser());
|
|
37
|
+
app.use(cors())
|
|
38
|
+
|
|
39
|
+
app.post("/ht/renew", (req, res) => {
|
|
40
|
+
// recreate a browser cookie from an existing server cookie, OR
|
|
41
|
+
// create a new server cookie that can later be used to recreate from.
|
|
42
|
+
return res.json({
|
|
43
|
+
userId: renewCookies(req, res, USER_COOKIE, `${USER_COOKIE}_srvr`),
|
|
44
|
+
anonymousId: renewCookies(req, res, ANON_COOKIE, `${ANON_COOKIE}_srvr`),
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.post("/ht/clear", (req, res) => {
|
|
49
|
+
// clear server cookies, e.g. if the user asks to clear all cookies.
|
|
50
|
+
return res.json({
|
|
51
|
+
userId: clearServerCookie(req, res, `${USER_COOKIE}_srvr`),
|
|
52
|
+
anonymousId: clearServerCookie(req, res, `${ANON_COOKIE}_srvr`),
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.listen(process.env.PORT || 3000, () => {
|
|
57
|
+
console.log(`Listening on port ${process.env.PORT}...`);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The HTTPCookieService must live on the same domain and IP address as your website's HTML document.
|
|
62
|
+
|
|
63
|
+
As one way to accomplish this, you could have NGINX both serve your HTML document and forward requests to the Node.js/Express server.
|
|
64
|
+
|
|
65
|
+
An **overly simplified** NGINX reverse proxy:
|
|
66
|
+
```
|
|
67
|
+
worker_processes 1;
|
|
68
|
+
|
|
69
|
+
events {
|
|
70
|
+
worker_connections 1024;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
http {
|
|
74
|
+
default_type application/octet-stream;
|
|
75
|
+
sendfile on;
|
|
76
|
+
keepalive_timeout 65;
|
|
77
|
+
|
|
78
|
+
server {
|
|
79
|
+
listen 8080;
|
|
80
|
+
server_name localhost;
|
|
81
|
+
|
|
82
|
+
location / {
|
|
83
|
+
root /Users/name/src/website/html;
|
|
84
|
+
index index.html index.htm;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
location /cdn {
|
|
88
|
+
#autoindex on;
|
|
89
|
+
alias /Users/name/src/website/cdn;
|
|
90
|
+
try_files $uri /index.html =404;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
location /ht {
|
|
94
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
95
|
+
proxy_set_header Host $host;
|
|
96
|
+
proxy_pass http://127.0.0.1:3000;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
include servers/*;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
**These server examples should not be used as is. They should be adapted to your setup and "productionized".**
|
|
2
|
+
|
|
3
|
+
An example HTTPCookieService written as a Next.js API Route.
|
|
4
|
+
|
|
5
|
+
```Javascript
|
|
6
|
+
import type { NextApiRequest, NextApiResponse } from "next";
|
|
7
|
+
|
|
8
|
+
const USER_COOKIE = "htjs_user_id";
|
|
9
|
+
const ANON_COOKIE = "htjs_anonymous_id";
|
|
10
|
+
|
|
11
|
+
function getDomain(request: NextApiRequest) {
|
|
12
|
+
const domain = request.headers.host?.toString() ?? "";
|
|
13
|
+
if (domain.startsWith("localhost")) return "localhost";
|
|
14
|
+
return domain;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renewCookies(request: NextApiRequest, response: NextApiResponse, browserName: string, serverName: string) {
|
|
18
|
+
const cookie = request.cookies[browserName] ?? request.cookies[serverName];
|
|
19
|
+
if (!cookie) return "";
|
|
20
|
+
const maxAge = 31_536_000; // 1 year in seconds
|
|
21
|
+
const domain = getDomain(request);
|
|
22
|
+
response.setHeader("Set-Cookie", [
|
|
23
|
+
...((response.getHeader("Set-Cookie") as string[]) ?? []),
|
|
24
|
+
`${browserName}=${cookie}; Max-Age=${maxAge}; Domain=${domain}; Path=/; SameSite=Lax;`,
|
|
25
|
+
`${serverName}=${cookie}; Max-Age=${maxAge}; Domain=${domain}; Path=/; SameSite=Lax; httpOnly=true;`,
|
|
26
|
+
]);
|
|
27
|
+
return cookie;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearServerCookie(request: NextApiRequest, response: NextApiResponse, serverName: string) {
|
|
31
|
+
const cookie = "";
|
|
32
|
+
const maxAge = 0;
|
|
33
|
+
const domain = getDomain(request);
|
|
34
|
+
response.setHeader("Set-Cookie", [
|
|
35
|
+
...((response.getHeader("Set-Cookie") as string[]) ?? []),
|
|
36
|
+
`${serverName}=${cookie}; Max-Age=${maxAge}; Domain=${domain}; Path=/; SameSite=Lax; httpOnly;`,
|
|
37
|
+
]);
|
|
38
|
+
return cookie;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function handler(request: NextApiRequest, response: NextApiResponse) {
|
|
42
|
+
if (request.method?.toUpperCase() !== "POST") {
|
|
43
|
+
response.status(404);
|
|
44
|
+
response.end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const slug = (request.query.slug as string).toLowerCase();
|
|
49
|
+
|
|
50
|
+
if (slug === "renew") {
|
|
51
|
+
response.status(200);
|
|
52
|
+
response.json({
|
|
53
|
+
userId: renewCookies(request, response, USER_COOKIE, `${USER_COOKIE}_srvr`),
|
|
54
|
+
anonymousId: renewCookies(request, response, ANON_COOKIE, `${ANON_COOKIE}_srvr`),
|
|
55
|
+
});
|
|
56
|
+
} else if (slug === "clear") {
|
|
57
|
+
response.status(200);
|
|
58
|
+
response.json({
|
|
59
|
+
userId: clearServerCookie(request, response, `${USER_COOKIE}_srvr`),
|
|
60
|
+
anonymousId: clearServerCookie(request, response, `${ANON_COOKIE}_srvr`),
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
response.status(404);
|
|
64
|
+
response.end();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The HTTPCookieService must live on the same domain and IP address as your website's HTML document.
|
|
70
|
+
|
|
71
|
+
As one way to accomplish this, you could create one Next.js project to serve both your HTML site and your API.
|
|
72
|
+
|
|
73
|
+
The above example code might live at `src/pages/api/ht/[slug].ts`, while your other non-API pages might live somewhere like `src/pages/blog/index.tsx`.
|
|
74
|
+
|
|
75
|
+
See [Next.js](https://nextjs.org/docs) for more information.
|
package/src/core/user/index.ts
CHANGED
|
@@ -140,6 +140,14 @@ export class User {
|
|
|
140
140
|
legacyUser.id && this.id(legacyUser.id)
|
|
141
141
|
legacyUser.traits && this.traits(legacyUser.traits)
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
// HTTPCookies require that localStorage values be synced to cookies
|
|
145
|
+
if (this.options.httpCookieService) {
|
|
146
|
+
this.identityStore.getAndSync(this.anonKey)
|
|
147
|
+
this.identityStore.getAndSync(this.idKey)
|
|
148
|
+
this.options.httpCookieService?.dispatchCreate()
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
autoBind(this)
|
|
144
152
|
}
|
|
145
153
|
|
package/src/core/user/tld.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import cookie from 'js-cookie'
|
|
1
|
+
import cookie, { CookieAttributes } from 'js-cookie'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Levels returns all levels of the given url.
|
|
@@ -45,11 +45,15 @@ export function tld(url: string): string | undefined {
|
|
|
45
45
|
|
|
46
46
|
const lvls = levels(parsedUrl)
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Test for the top most domain that the browser allows
|
|
49
49
|
for (let i = 0; i < lvls.length; ++i) {
|
|
50
|
-
const cname =
|
|
50
|
+
const cname = Math.round(Math.random() * 10_000).toString()
|
|
51
51
|
const domain = lvls[i]
|
|
52
|
-
const opts = {
|
|
52
|
+
const opts = {
|
|
53
|
+
domain: '.' + domain,
|
|
54
|
+
path: '/',
|
|
55
|
+
sameSite: 'Lax',
|
|
56
|
+
} as CookieAttributes
|
|
53
57
|
|
|
54
58
|
try {
|
|
55
59
|
// cookie access throw an error if the library is ran inside a sandboxed environment (e.g. sandboxed iframe)
|
package/src/generated/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is generated.
|
|
2
|
-
export const version = '1.
|
|
2
|
+
export const version = '1.3.1'
|