@ht-sdks/events-sdk-js-browser 1.0.4 → 1.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.
Files changed (147) hide show
  1. package/README.md +22 -22
  2. package/dist/cjs/browser/index.js +36 -28
  3. package/dist/cjs/browser/index.js.map +1 -1
  4. package/dist/cjs/browser/standalone-analytics.js +1 -1
  5. package/dist/cjs/browser/standalone-analytics.js.map +1 -1
  6. package/dist/cjs/core/analytics/index.js +1 -1
  7. package/dist/cjs/core/analytics/index.js.map +1 -1
  8. package/dist/cjs/core/arguments-resolver/index.js.map +1 -1
  9. package/dist/cjs/core/buffer/index.js.map +1 -1
  10. package/dist/cjs/core/context/index.js.map +1 -1
  11. package/dist/cjs/core/events/index.js.map +1 -1
  12. package/dist/cjs/core/http-cookies/index.js +166 -0
  13. package/dist/cjs/core/http-cookies/index.js.map +1 -0
  14. package/dist/cjs/core/page/add-page-context.js.map +1 -1
  15. package/dist/cjs/core/user/index.js +25 -5
  16. package/dist/cjs/core/user/index.js.map +1 -1
  17. package/dist/cjs/generated/version.js +1 -1
  18. package/dist/cjs/lib/global-analytics-helper.js +1 -1
  19. package/dist/cjs/lib/global-analytics-helper.js.map +1 -1
  20. package/dist/cjs/lib/merged-options.js +1 -1
  21. package/dist/cjs/lib/parse-cdn.js +1 -1
  22. package/dist/cjs/lib/parse-cdn.js.map +1 -1
  23. package/dist/cjs/lib/to-facade.js.map +1 -1
  24. package/dist/cjs/node/index.js +1 -1
  25. package/dist/cjs/node/index.js.map +1 -1
  26. package/dist/cjs/plugins/ajs-destination/utils.js +1 -1
  27. package/dist/cjs/plugins/ajs-destination/utils.js.map +1 -1
  28. package/dist/cjs/plugins/analytics-node/index.js +2 -2
  29. package/dist/cjs/plugins/analytics-node/index.js.map +1 -1
  30. package/dist/cjs/plugins/env-enrichment/index.js.map +1 -1
  31. package/dist/cjs/plugins/hightouchio/batched-dispatcher.js.map +1 -1
  32. package/dist/cjs/plugins/legacy-video-plugins/index.js +4 -4
  33. package/dist/cjs/plugins/legacy-video-plugins/index.js.map +1 -1
  34. package/dist/cjs/plugins/middleware/index.js.map +1 -1
  35. package/dist/pkg/browser/index.js +35 -27
  36. package/dist/pkg/browser/index.js.map +1 -1
  37. package/dist/pkg/browser/standalone-analytics.js +2 -2
  38. package/dist/pkg/browser/standalone-analytics.js.map +1 -1
  39. package/dist/pkg/core/analytics/index.js +1 -1
  40. package/dist/pkg/core/analytics/index.js.map +1 -1
  41. package/dist/pkg/core/arguments-resolver/index.js.map +1 -1
  42. package/dist/pkg/core/buffer/index.js.map +1 -1
  43. package/dist/pkg/core/context/index.js.map +1 -1
  44. package/dist/pkg/core/events/index.js.map +1 -1
  45. package/dist/pkg/core/http-cookies/index.js +163 -0
  46. package/dist/pkg/core/http-cookies/index.js.map +1 -0
  47. package/dist/pkg/core/page/add-page-context.js.map +1 -1
  48. package/dist/pkg/core/user/index.js +25 -5
  49. package/dist/pkg/core/user/index.js.map +1 -1
  50. package/dist/pkg/generated/version.js +1 -1
  51. package/dist/pkg/lib/global-analytics-helper.js +1 -1
  52. package/dist/pkg/lib/global-analytics-helper.js.map +1 -1
  53. package/dist/pkg/lib/merged-options.js +1 -1
  54. package/dist/pkg/lib/parse-cdn.js +1 -1
  55. package/dist/pkg/lib/parse-cdn.js.map +1 -1
  56. package/dist/pkg/lib/to-facade.js.map +1 -1
  57. package/dist/pkg/node/index.js +1 -1
  58. package/dist/pkg/node/index.js.map +1 -1
  59. package/dist/pkg/plugins/ajs-destination/utils.js +1 -1
  60. package/dist/pkg/plugins/ajs-destination/utils.js.map +1 -1
  61. package/dist/pkg/plugins/analytics-node/index.js +2 -2
  62. package/dist/pkg/plugins/analytics-node/index.js.map +1 -1
  63. package/dist/pkg/plugins/env-enrichment/index.js.map +1 -1
  64. package/dist/pkg/plugins/hightouchio/batched-dispatcher.js.map +1 -1
  65. package/dist/pkg/plugins/legacy-video-plugins/index.js +6 -6
  66. package/dist/pkg/plugins/legacy-video-plugins/index.js.map +1 -1
  67. package/dist/pkg/plugins/middleware/index.js.map +1 -1
  68. package/dist/types/browser/index.d.ts +12 -12
  69. package/dist/types/browser/index.d.ts.map +1 -1
  70. package/dist/types/browser/standalone-interface.d.ts +1 -1
  71. package/dist/types/browser/standalone-interface.d.ts.map +1 -1
  72. package/dist/types/core/analytics/index.d.ts +12 -3
  73. package/dist/types/core/analytics/index.d.ts.map +1 -1
  74. package/dist/types/core/analytics/interfaces.d.ts +5 -5
  75. package/dist/types/core/analytics/interfaces.d.ts.map +1 -1
  76. package/dist/types/core/arguments-resolver/index.d.ts +2 -2
  77. package/dist/types/core/arguments-resolver/index.d.ts.map +1 -1
  78. package/dist/types/core/buffer/index.d.ts +10 -10
  79. package/dist/types/core/buffer/index.d.ts.map +1 -1
  80. package/dist/types/core/context/index.d.ts +3 -3
  81. package/dist/types/core/context/index.d.ts.map +1 -1
  82. package/dist/types/core/events/index.d.ts +8 -8
  83. package/dist/types/core/events/index.d.ts.map +1 -1
  84. package/dist/types/core/events/interfaces.d.ts +2 -2
  85. package/dist/types/core/events/interfaces.d.ts.map +1 -1
  86. package/dist/types/core/http-cookies/index.d.ts +53 -0
  87. package/dist/types/core/http-cookies/index.d.ts.map +1 -0
  88. package/dist/types/core/page/add-page-context.d.ts +2 -2
  89. package/dist/types/core/page/add-page-context.d.ts.map +1 -1
  90. package/dist/types/core/page/get-page-context.d.ts +1 -1
  91. package/dist/types/core/stats/remote-metrics.d.ts +1 -1
  92. package/dist/types/core/user/index.d.ts +5 -0
  93. package/dist/types/core/user/index.d.ts.map +1 -1
  94. package/dist/types/generated/version.d.ts +1 -1
  95. package/dist/types/index.d.ts +1 -1
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/lib/global-analytics-helper.d.ts +4 -4
  98. package/dist/types/lib/global-analytics-helper.d.ts.map +1 -1
  99. package/dist/types/lib/merged-options.d.ts +1 -1
  100. package/dist/types/lib/to-facade.d.ts +4 -4
  101. package/dist/types/lib/to-facade.d.ts.map +1 -1
  102. package/dist/types/plugins/analytics-node/index.d.ts +2 -2
  103. package/dist/types/plugins/analytics-node/index.d.ts.map +1 -1
  104. package/dist/types/plugins/legacy-video-plugins/index.d.ts +1 -1
  105. package/dist/types/plugins/legacy-video-plugins/index.d.ts.map +1 -1
  106. package/dist/types/plugins/middleware/index.d.ts +4 -4
  107. package/dist/types/plugins/middleware/index.d.ts.map +1 -1
  108. package/dist/umd/{ajs-destination.bundle.5a985a542b0de08e3e49.js → ajs-destination.bundle.48266e86bbbc2531ac42.js} +2 -2
  109. package/dist/umd/{ajs-destination.bundle.5a985a542b0de08e3e49.js.map → ajs-destination.bundle.48266e86bbbc2531ac42.js.map} +1 -1
  110. package/dist/umd/events.min.js +1 -1
  111. package/dist/umd/events.min.js.map +1 -1
  112. package/dist/umd/index.js +1 -1
  113. package/dist/umd/index.js.map +1 -1
  114. package/dist/umd/legacyVideos.bundle.6250709892bb05b68333.js.map +1 -1
  115. package/package.json +3 -3
  116. package/src/browser/index.ts +27 -20
  117. package/src/browser/standalone-analytics.ts +3 -3
  118. package/src/browser/standalone-interface.ts +1 -1
  119. package/src/core/analytics/index.ts +18 -4
  120. package/src/core/analytics/interfaces.ts +5 -5
  121. package/src/core/arguments-resolver/index.ts +2 -2
  122. package/src/core/buffer/index.ts +4 -4
  123. package/src/core/context/index.ts +3 -3
  124. package/src/core/events/index.ts +22 -19
  125. package/src/core/events/interfaces.ts +2 -2
  126. package/src/core/http-cookies/README.md +194 -0
  127. package/src/core/http-cookies/index.ts +152 -0
  128. package/src/core/page/add-page-context.ts +2 -2
  129. package/src/core/page/get-page-context.ts +1 -1
  130. package/src/core/stats/remote-metrics.ts +1 -1
  131. package/src/core/user/index.ts +34 -4
  132. package/src/generated/version.ts +1 -1
  133. package/src/index.ts +1 -1
  134. package/src/lib/global-analytics-helper.ts +4 -4
  135. package/src/lib/merged-options.ts +1 -1
  136. package/src/lib/parse-cdn.ts +1 -1
  137. package/src/lib/to-facade.ts +7 -4
  138. package/src/node/index.ts +1 -1
  139. package/src/plugins/ajs-destination/types.ts +1 -1
  140. package/src/plugins/ajs-destination/utils.ts +1 -1
  141. package/src/plugins/analytics-node/index.ts +5 -5
  142. package/src/plugins/env-enrichment/index.ts +2 -2
  143. package/src/plugins/hightouchio/batched-dispatcher.ts +3 -3
  144. package/src/plugins/legacy-video-plugins/index.ts +4 -4
  145. package/src/plugins/middleware/index.ts +10 -10
  146. package/src/test-helpers/fixtures/cdn-settings.ts +2 -2
  147. package/src/test-helpers/test-writekeys.ts +1 -1
@@ -0,0 +1,194 @@
1
+ # HTTPOnly Cookies
2
+
3
+ ## "Browser Cookies" vs "Server Cookies"
4
+
5
+ **Cookies are serialized sets of key-value pairs that the browser can send to the server when making an HTTP request (e.g. on the `Cookie` header)**. Traditionally, the browser does this to identify itself when calling the server. For example, it might receive a cookie when calling a `/login` route, and then continue to send this cookie on subsequent requests, proving that the user is still logged-in. Alternatively, browsers can use cookies just for storing local data, like localStorage.
6
+
7
+ Normally, **both** the client and the server can CRUD cookies.
8
+
9
+ However, as a security measure, browsers restrict Javascript access to cookies containing the `HTTPOnly` property. These cookies are **only** intended to be created and read by the server.
10
+
11
+ ## "Browser Cookies" for event attribution
12
+
13
+ Certain browsers (e.g. Safari) limit "Browser Cookies" to a 7 day expiry.
14
+
15
+ A user visiting a website on both 01/01/2023 and 01/14/2023 will look like two different users. The browser will delete the user's "anonymousId cookie" before the user begins their second session on 01/14/2023.
16
+
17
+ ## "Server Cookies" for event attribution
18
+
19
+ These expiry limits don't have to apply to "Server Cookies".
20
+
21
+ A user visiting a website on both 01/01/2023 and 01/14/2023 can still look like the same user, provided that there is a way to "regenerate" the user's same "anonymousId cookie" from the earlier session. To do this, the following must happen:
22
+ 1. User begins their session on 01/01/2023
23
+ 1. Events SDK creates an anonymousId "browser cookie"
24
+ 1. Events SDK sends "browser cookie" to `$server` and receives back an HTTPOnly cookie with the same anonymousId
25
+ 1. HTTPOnly cookie remains on the user's device
26
+ 1. User begins their second session on 01/14/2023
27
+ 1. Events SDK sends the HTTPOnly cookie to `$server` and receives back a "browser cookie" with the same anonymousId as the first session
28
+
29
+ **The `$server` must be the same server that serves your website.** Certain browsers (e.g. Safari) will still enforce a 7 day expiry--even for "server cookies"--unless the following criteria are met:
30
+ 1. The `$server` providing the HTTPOnly cookie must be on the same domain as the website.
31
+ 1. If `$server` is on a subdomain of the website, its IP address must match the IP address that served the main HTML document.
32
+
33
+ Routing a subdomain via DNS will not suffice. You'll need **one of** the following:
34
+ - A **webserver** that serves both your HTML document and an API (e.g. something like Django, Rails, Spring, etc)
35
+ - A **reverse proxy** that can forward requests for your HTML document to one place, your API requests to another, and make it look like it's all on one server (e.g. NGINX, Caddy, etc)
36
+ - A **CDN** that can run programmatic logic when matching certain requests (e.g. Lambda@Edge, Clouflare Workers, etc)
37
+
38
+ ## Client SDK Setup
39
+
40
+ ```javascript
41
+ import { HtEventsBrowser } from '@ht-sdks/events-sdk-js-browser'
42
+
43
+ const htevents = HtEventsBrowser.load(
44
+ { writeKey: '<YOUR_WRITE_KEY>'},
45
+ {
46
+ apiHost: "us-east-1.hightouch-events.com", // HtEvents API remains the same
47
+ httpCookieServiceOptions: {
48
+ clearUrl: 'ht/clear', // route hosted on *your* domain and infra
49
+ renewUrl: 'ht/renew', // route hosted on *your* domain and infra
50
+ }
51
+ },
52
+ )
53
+
54
+ htevents.identify('hello world')
55
+
56
+ document.body?.addEventListener('click', () => {
57
+ htevents.track('document body clicked!')
58
+ })
59
+ ```
60
+
61
+ ## Server Setup
62
+
63
+ The Events SDK expects to interact with a customer's `$server` that implements a specific spec for two routes. You can name the endpoints whatever you want.
64
+
65
+ ### An API for **creating** server and browser cookies
66
+
67
+ This route should look for the following **browser** cookies (from Events SDK):
68
+ * `request.headers.get('Cookie')["htjs_anonymous_id"]`
69
+ * `request.headers.get('Cookie')["htjs_user_id"]`
70
+
71
+ This route should return these values as **server** cookies:
72
+ * `response.cookie("htjs_anonymous_id_srvr", anonVal, {httpOnly:true, ...})`
73
+ * `response.cookie("htjs_user_id_srvr", userIdVal, {httpOnly:true, ...})`
74
+
75
+ If there are no browser cookies found, return any server cookies as browser cookies:
76
+ * `response.cookie("htjs_anonymous_id", anonVal, ...)`
77
+ * `response.cookie("htjs_user_id", userIdVal, ...)`
78
+
79
+ ### An API for **clearing** server cookies
80
+
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});`
84
+
85
+ ### API Spec
86
+ The spec of the actual `request` and `response` payloads are kept intentionally vague. The spec should fit a variety of server environments.
87
+
88
+ The Events SDK only requires that the server: A) handles cookies and B) returns a `200` statuscode.
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
+ ```
149
+
150
+ A **very** simplified NGINX reverse proxy serving your document and API from the same domain:
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
+ }
183
+
184
+ }
185
+
186
+ include servers/*;
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.
191
+
192
+ ## More information
193
+ - Safari: https://webkit.org/blog/9521/intelligent-tracking-prevention-2-3/
194
+
@@ -0,0 +1,152 @@
1
+ import { fetch } from '../../lib/fetch'
2
+
3
+ type DeferredRequest = () => Promise<Response>
4
+
5
+ export type HTTPCookieServiceOptions = {
6
+ renewUrl: string
7
+ clearUrl: string
8
+ retries?: number
9
+ backoff?: number
10
+ flushInterval?: number
11
+ }
12
+
13
+ /**
14
+ * `HTTPCookieService.load(...)` should be awaited inside `loadAnalytics(...)`.
15
+ * If the server can recreate an expired "Browser Cookie", by inspecting existing
16
+ * "Server Cookies", we should delay dispatching events until receiving the Cookie.
17
+ *
18
+ * dispatch$Method class methods should be used after any cookie interactions:
19
+ * `dispatchCreate()` should be called after creating any new browser cookies.
20
+ * `dispatchClear()` should be called after clearing any browser cookies.
21
+ *
22
+ * Manual `startQueueConsumer()` or `stopQueueConsumer()` is not usually necessary.
23
+ *
24
+ * Glossary:
25
+ * "Server Cookies": `HTTPOnly:true`, stored on client, but only server can access.
26
+ * "Browser Cookies": `HTTPOnly:false`, stored on client, and both client and server can access.
27
+ */
28
+ export class HTTPCookieService {
29
+ private queue: DeferredRequest[]
30
+ private renewUrl: string
31
+ private clearUrl: string
32
+ private retries: number
33
+ private backoff: number
34
+ private flushInterval: number
35
+ private flushIntervalId?: NodeJS.Timer
36
+
37
+ private constructor(options: HTTPCookieServiceOptions) {
38
+ this.renewUrl = options.renewUrl
39
+ this.clearUrl = options.clearUrl
40
+ this.backoff = options.backoff ?? 300
41
+ this.retries = options.retries ?? 3
42
+ this.flushInterval = options.flushInterval ?? 1000
43
+ this.queue = []
44
+ }
45
+
46
+ static async load(
47
+ options: HTTPCookieServiceOptions
48
+ ): Promise<HTTPCookieService> {
49
+ const cookieService = new HTTPCookieService(options)
50
+
51
+ // renew any existing HTTPCookies already on the device
52
+ // we want `load()` to block on this, so await directly instead of calling dispatch
53
+ const req = cookieService.sendHTTPCookies(options.renewUrl)
54
+ await retry(req, cookieService.retries, cookieService.backoff).catch(
55
+ console.error
56
+ )
57
+
58
+ // consume HTTPCookie actions, sequentially, as needed
59
+ cookieService.startQueueConsumer()
60
+
61
+ return cookieService
62
+ }
63
+
64
+ dispatchCreate() {
65
+ this.queue.push(this.sendHTTPCookies(this.renewUrl))
66
+ }
67
+
68
+ dispatchClear() {
69
+ this.queue.push(this.sendHTTPCookies(this.clearUrl))
70
+ }
71
+
72
+ startQueueConsumer() {
73
+ if (this.flushIntervalId) {
74
+ console.error('HTTPCookie queue consumer is already running.')
75
+ return
76
+ }
77
+ const bound = this.consumeQueue.bind(this)
78
+ this.flushIntervalId = setInterval(
79
+ () => bound().catch(console.error),
80
+ this.flushInterval
81
+ )
82
+ }
83
+
84
+ stopQueueConsumer() {
85
+ if (!this.flushIntervalId) {
86
+ console.error('HTTPCookie queue consumer is already stopped.')
87
+ return
88
+ }
89
+ clearInterval(this.flushIntervalId)
90
+ this.flushIntervalId = undefined
91
+ }
92
+
93
+ private sendHTTPCookies(serviceUrl: string): DeferredRequest {
94
+ return async function (): Promise<Response> {
95
+ return await fetch(serviceUrl, {
96
+ credentials: 'include',
97
+ headers: {
98
+ Accept: 'application/json',
99
+ 'Content-Type': 'application/json',
100
+ },
101
+ method: 'post',
102
+ body: JSON.stringify({
103
+ sentAt: new Date().toISOString(),
104
+ }),
105
+ })
106
+ }
107
+ }
108
+
109
+ /**
110
+ * This queue exists to avoid race conditions.
111
+ *
112
+ * Customer-developers may not `await` all promises.
113
+ *
114
+ * Therefore, introducing async code into analytics.track(), etc
115
+ * could create race conditions in customer code.
116
+ *
117
+ * The queue enforces: if someone calls analytics.clear()
118
+ * before calling analytics.identify(), the cookie service
119
+ * will consume those actions, sequentially, even if no promises
120
+ * are awaited.
121
+ */
122
+ private async consumeQueue() {
123
+ while (this.queue.length > 0) {
124
+ const req = this.queue.shift() as DeferredRequest
125
+ await retry(req, this.retries, this.backoff).catch(console.error)
126
+ }
127
+ }
128
+ }
129
+
130
+ async function sleep(delayMS: number): Promise<void> {
131
+ return new Promise((resolve) => setTimeout(resolve, delayMS))
132
+ }
133
+
134
+ async function retry(
135
+ req: DeferredRequest,
136
+ retries: number,
137
+ backoff: number
138
+ ): Promise<Response> {
139
+ while (retries >= 0) {
140
+ try {
141
+ return await req().then((res) => {
142
+ if (res.ok) return res
143
+ throw new Error(`Status: ${res.status} ${res.statusText}`)
144
+ })
145
+ } catch (error) {
146
+ retries -= 1
147
+ if (retries <= 0) throw error
148
+ await sleep(backoff)
149
+ }
150
+ }
151
+ throw Error('HtEvents: Problem with DeferredRequest')
152
+ }
@@ -1,5 +1,5 @@
1
1
  import { pick } from '../../lib/pick'
2
- import { EventProperties, SegmentEvent } from '../events'
2
+ import { EventProperties, HightouchEvent } from '../events'
3
3
  import { getDefaultPageContext } from './get-page-context'
4
4
 
5
5
  /**
@@ -9,7 +9,7 @@ import { getDefaultPageContext } from './get-page-context'
9
9
  * We prefer not to add this information to this function, as it increases the main bundle size.
10
10
  */
11
11
  export const addPageContext = (
12
- event: SegmentEvent,
12
+ event: HightouchEvent,
13
13
  pageCtx = getDefaultPageContext()
14
14
  ): void => {
15
15
  const evtCtx = event.context! // Context should be set earlier in the flow
@@ -1,7 +1,7 @@
1
1
  import { isPlainObject } from '@ht-sdks/events-sdk-js-core'
2
2
 
3
3
  /**
4
- * Final Page Context object expected in the Segment Event context
4
+ * Final Page Context object expected in the Hightouch Event context
5
5
  */
6
6
  export interface PageContext {
7
7
  path: string
@@ -11,7 +11,7 @@ export interface MetricsOptions {
11
11
  }
12
12
 
13
13
  /**
14
- * Type expected by the segment metrics API endpoint
14
+ * Type expected by the hightouch metrics API endpoint
15
15
  */
16
16
  type RemoteMetric = {
17
17
  type: 'Counter'
@@ -21,6 +21,7 @@ import {
21
21
  hasSessionExpired,
22
22
  updateSessionExpiration,
23
23
  } from '../session'
24
+ import type { HTTPCookieService } from '../http-cookies'
24
25
 
25
26
  export type ID = string | null | undefined
26
27
 
@@ -32,6 +33,11 @@ export interface UserOptions {
32
33
  localStorageFallbackDisabled?: boolean
33
34
  persist?: boolean
34
35
 
36
+ /**
37
+ * Replicates "BrowserCookie" actions against a matching "ServerCookie".
38
+ */
39
+ httpCookieService?: HTTPCookieService
40
+
35
41
  cookie?: {
36
42
  key?: string
37
43
  oldKey?: string
@@ -145,11 +151,22 @@ export class User {
145
151
  const prevId = this.identityStore.getAndSync(this.idKey)
146
152
 
147
153
  if (id !== undefined) {
154
+ const clearingIdentity = id === null
155
+ const changingIdentity = id !== prevId && prevId !== null && id !== null
156
+ const creatingIdentity = id !== prevId && prevId === null && id !== null
157
+
148
158
  this.identityStore.set(this.idKey, id)
149
159
 
150
- const changingIdentity = id !== prevId && prevId !== null && id !== null
160
+ if (clearingIdentity) {
161
+ this.options?.httpCookieService?.dispatchClear()
162
+ }
163
+
151
164
  if (changingIdentity) {
152
- this.anonymousId(null)
165
+ this.anonymousId(null) // this also runs dispatchClear()
166
+ }
167
+
168
+ if (changingIdentity || creatingIdentity) {
169
+ this.options?.httpCookieService?.dispatchCreate()
153
170
  }
154
171
  }
155
172
 
@@ -176,28 +193,36 @@ export class User {
176
193
 
177
194
  if (id === undefined) {
178
195
  let val = this.identityStore.getAndSync(this.anonKey)
196
+ let migrated = false
179
197
 
180
198
  // support anonymousId migration from other analytics providers
181
199
  if (!val) {
182
200
  val = decryptRudderHtValue(
183
201
  this.identityStore.getAndSync(rudderHtAnonymousIdKey) ?? ''
184
202
  )
203
+ migrated = Boolean(val)
185
204
  if (val) this.identityStore.set(this.anonKey, val)
186
205
  }
187
206
  if (!val) {
188
207
  val = this.identityStore.getAndSync(segmentAnonymousIdKey)
208
+ migrated = Boolean(val)
189
209
  if (val) this.identityStore.set(this.anonKey, val)
190
210
  }
191
211
  if (!val) {
192
212
  val = decryptRudderValue(
193
213
  this.identityStore.getAndSync(rudderAnonymousIdKey) ?? ''
194
214
  )
215
+ migrated = Boolean(val)
195
216
  if (val) this.identityStore.set(this.anonKey, val)
196
217
  }
197
218
  if (!val) {
198
219
  val = this.legacySIO()?.[0] ?? null
199
220
  }
200
221
 
222
+ if (migrated) {
223
+ this.options?.httpCookieService?.dispatchCreate()
224
+ }
225
+
201
226
  if (val) {
202
227
  return val
203
228
  }
@@ -205,11 +230,16 @@ export class User {
205
230
 
206
231
  if (id === null) {
207
232
  this.identityStore.set(this.anonKey, null)
208
- return this.identityStore.getAndSync(this.anonKey)
233
+ const clearedVal = this.identityStore.getAndSync(this.anonKey)
234
+ this.options?.httpCookieService?.dispatchClear()
235
+ return clearedVal
209
236
  }
210
237
 
211
238
  this.identityStore.set(this.anonKey, id ?? uuid())
212
- return this.identityStore.getAndSync(this.anonKey)
239
+ const syncedVal = this.identityStore.getAndSync(this.anonKey)
240
+
241
+ this.options?.httpCookieService?.dispatchCreate()
242
+ return syncedVal
213
243
  }
214
244
 
215
245
  traits = (traits?: Traits | null): Traits | undefined => {
@@ -1,2 +1,2 @@
1
1
  // This file is generated.
2
- export const version = '1.0.4'
2
+ export const version = '1.2.0'
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ export * from './core/events'
7
7
  export * from './core/plugin'
8
8
  export * from './core/user'
9
9
 
10
- export type { AnalyticsSnippet } from './browser/standalone-interface'
10
+ export type { HtEventsSnippet } from './browser/standalone-interface'
11
11
  export type { MiddlewareFunction } from './plugins/middleware'
12
12
  export { getGlobalAnalytics } from './lib/global-analytics-helper'
13
13
  export { UniversalStorage, Store, StorageObject } from './core/storage'
@@ -1,4 +1,4 @@
1
- import { AnalyticsSnippet } from '../browser/standalone-interface'
1
+ import { HtEventsSnippet } from '../browser/standalone-interface'
2
2
 
3
3
  /**
4
4
  * Stores the global window analytics key
@@ -8,9 +8,9 @@ let _globalAnalyticsKey = 'htevents'
8
8
  /**
9
9
  * Gets the global analytics/buffer
10
10
  * @param key name of the window property where the buffer is stored (default: analytics)
11
- * @returns AnalyticsSnippet
11
+ * @returns HtEventsSnippet
12
12
  */
13
- export function getGlobalAnalytics(): AnalyticsSnippet | undefined {
13
+ export function getGlobalAnalytics(): HtEventsSnippet | undefined {
14
14
  return (window as any)[_globalAnalyticsKey]
15
15
  }
16
16
 
@@ -26,6 +26,6 @@ export function setGlobalAnalyticsKey(key: string) {
26
26
  * Sets the global analytics object
27
27
  * @param analytics analytics snippet
28
28
  */
29
- export function setGlobalAnalytics(analytics: AnalyticsSnippet): void {
29
+ export function setGlobalAnalytics(analytics: HtEventsSnippet): void {
30
30
  ;(window as any)[_globalAnalyticsKey] = analytics
31
31
  }
@@ -5,7 +5,7 @@ import { LegacySettings } from '../browser'
5
5
  * Merge legacy settings and initialized Integration option overrides.
6
6
  *
7
7
  * This will merge any options that were passed from initialization into
8
- * overrides for settings that are returned by the Segment CDN.
8
+ * overrides for settings that are returned by the Hightouch CDN.
9
9
  *
10
10
  * i.e. this allows for passing options directly into destinations from
11
11
  * the Analytics constructor.
@@ -45,7 +45,7 @@ export const getCDN = (): string => {
45
45
  // it's possible that the CDN is not found in the page because:
46
46
  // - the script is loaded through a proxy
47
47
  // - the script is removed after execution
48
- // in this case, we fall back to the default Segment CDN
48
+ // in this case, we fall back to the default Hightouch CDN
49
49
  return `https://cdn.hightouch-events.com`
50
50
  }
51
51
  }
@@ -8,13 +8,16 @@ import {
8
8
  Screen,
9
9
  Track,
10
10
  } from '@segment/facade'
11
- import { SegmentEvent } from '../core/events'
11
+ import { HightouchEvent } from '../core/events'
12
12
 
13
- export type SegmentFacade = Facade<SegmentEvent> & {
14
- obj: SegmentEvent
13
+ export type SegmentFacade = Facade<HightouchEvent> & {
14
+ obj: HightouchEvent
15
15
  }
16
16
 
17
- export function toFacade(evt: SegmentEvent, options?: Options): SegmentFacade {
17
+ export function toFacade(
18
+ evt: HightouchEvent,
19
+ options?: Options
20
+ ): SegmentFacade {
18
21
  let fcd = new Facade(evt, options)
19
22
 
20
23
  if (evt.type === 'track') {
package/src/node/index.ts CHANGED
@@ -20,7 +20,7 @@ export class AnalyticsNode {
20
20
 
21
21
  const nodeSettings = {
22
22
  writeKey: settings.writeKey,
23
- name: 'analytics-node-next',
23
+ name: 'events-sdk-js-node',
24
24
  type: 'after' as Plugin['type'],
25
25
  version: 'latest',
26
26
  }
@@ -18,7 +18,7 @@ export interface LegacyIntegration extends Emitter {
18
18
  alias?: (event: Alias) => void | Promise<void>
19
19
  group?: (event: Group) => void | Promise<void>
20
20
 
21
- // Segment.io specific
21
+ // Hightouch.io specific
22
22
  ontrack?: (event: Track) => void | Promise<void>
23
23
  onidentify?: (event: Identify) => void | Promise<void>
24
24
  onpage?: (event: Page) => void | Promise<void>
@@ -15,7 +15,7 @@ export const isInstallableIntegration = (
15
15
  // checking for iterable is a quick fix we need in place to prevent
16
16
  // errors showing Iterable as a failed destiantion. Ideally, we should
17
17
  // fix the Iterable metadata instead, but that's a longer process.
18
- return !name.startsWith('Segment') && name !== 'Iterable' && deviceMode
18
+ return !name.startsWith('Hightouch') && name !== 'Iterable' && deviceMode
19
19
  }
20
20
 
21
21
  export const isDisabledIntegration = (
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from '../../core/plugin'
2
2
  import { Context } from '../../core/context'
3
- import { SegmentEvent } from '../../core/events'
3
+ import { HightouchEvent } from '../../core/events'
4
4
  import fetch from 'node-fetch'
5
5
  import { version } from '../../generated/version'
6
6
 
@@ -14,16 +14,16 @@ interface AnalyticsNodeSettings {
14
14
  const btoa = (val: string): string => Buffer.from(val).toString('base64')
15
15
 
16
16
  export async function post(
17
- event: SegmentEvent,
17
+ event: HightouchEvent,
18
18
  writeKey: string
19
- ): Promise<SegmentEvent> {
19
+ ): Promise<HightouchEvent> {
20
20
  const res = await fetch(
21
21
  `https://us-east-1.hightouch-events.com/v1/${event.type}`,
22
22
  {
23
23
  method: 'POST',
24
24
  headers: {
25
25
  'Content-Type': 'application/json',
26
- 'User-Agent': 'analytics-node-next/latest',
26
+ 'User-Agent': 'events-sdk-js-node/latest',
27
27
  Authorization: `Basic ${btoa(writeKey)}`,
28
28
  },
29
29
  body: JSON.stringify(event),
@@ -39,7 +39,7 @@ export async function post(
39
39
 
40
40
  export function analyticsNode(settings: AnalyticsNodeSettings): Plugin {
41
41
  const send = async (ctx: Context): Promise<Context> => {
42
- ctx.updateEvent('context.library.name', 'analytics-node-next')
42
+ ctx.updateEvent('context.library.name', 'events-sdk-js-node')
43
43
  ctx.updateEvent('context.library.version', version)
44
44
  ctx.updateEvent('_metadata.nodeVersion', process.versions.node)
45
45
 
@@ -2,7 +2,7 @@ import jar from 'js-cookie'
2
2
  import type { Context } from '../../core/context'
3
3
  import type { Plugin } from '../../core/plugin'
4
4
  import { version } from '../../generated/version'
5
- import { SegmentEvent } from '../../core/events'
5
+ import { HightouchEvent } from '../../core/events'
6
6
  import { Campaign, PluginType } from '@ht-sdks/events-sdk-js-core'
7
7
  import { getVersionType } from '../../lib/version-type'
8
8
  import { tld } from '../../core/user/tld'
@@ -84,7 +84,7 @@ export function ampId(): string | undefined {
84
84
 
85
85
  function referrerId(
86
86
  query: string,
87
- ctx: SegmentEvent['context'],
87
+ ctx: HightouchEvent['context'],
88
88
  disablePersistance: boolean
89
89
  ): void {
90
90
  const storage = new UniversalStorage<{
@@ -1,4 +1,4 @@
1
- import { SegmentEvent } from '../../core/events'
1
+ import { HightouchEvent } from '../../core/events'
2
2
  import { fetch } from '../../lib/fetch'
3
3
  import { onPageChange } from '../../lib/on-page-change'
4
4
 
@@ -59,11 +59,11 @@ export default function batch(
59
59
  return
60
60
  }
61
61
 
62
- const writeKey = (batch[0] as SegmentEvent)?.writeKey
62
+ const writeKey = (batch[0] as HightouchEvent)?.writeKey
63
63
 
64
64
  // Remove sentAt from every event as batching only needs a single timestamp
65
65
  const updatedBatch = batch.map((event) => {
66
- const { sentAt, ...newEvent } = event as SegmentEvent
66
+ const { sentAt, ...newEvent } = event as HightouchEvent
67
67
  return newEvent
68
68
  })
69
69