@ht-sdks/events-sdk-js-browser 1.1.0 → 1.3.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 (95) hide show
  1. package/README.md +72 -4
  2. package/dist/cjs/browser/index.js +76 -42
  3. package/dist/cjs/browser/index.js.map +1 -1
  4. package/dist/cjs/core/analytics/index.js +1 -1
  5. package/dist/cjs/core/analytics/index.js.map +1 -1
  6. package/dist/cjs/core/http-cookies/index.js +174 -0
  7. package/dist/cjs/core/http-cookies/index.js.map +1 -0
  8. package/dist/cjs/core/user/index.js +32 -6
  9. package/dist/cjs/core/user/index.js.map +1 -1
  10. package/dist/cjs/core/user/tld.js +7 -3
  11. package/dist/cjs/core/user/tld.js.map +1 -1
  12. package/dist/cjs/generated/version.js +1 -1
  13. package/dist/cjs/index.js +3 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/node/index.js +1 -1
  16. package/dist/cjs/node/index.js.map +1 -1
  17. package/dist/cjs/plugins/analytics-node/index.js +2 -2
  18. package/dist/cjs/plugins/analytics-node/index.js.map +1 -1
  19. package/dist/cjs/plugins/destinations/destination.js +81 -0
  20. package/dist/cjs/plugins/destinations/destination.js.map +1 -0
  21. package/dist/cjs/plugins/destinations/google-tag-manager.js +47 -0
  22. package/dist/cjs/plugins/destinations/google-tag-manager.js.map +1 -0
  23. package/dist/cjs/plugins/destinations/index.js +22 -0
  24. package/dist/cjs/plugins/destinations/index.js.map +1 -0
  25. package/dist/cjs/plugins/destinations/types.js +3 -0
  26. package/dist/cjs/plugins/destinations/types.js.map +1 -0
  27. package/dist/pkg/browser/index.js +76 -42
  28. package/dist/pkg/browser/index.js.map +1 -1
  29. package/dist/pkg/core/analytics/index.js +1 -1
  30. package/dist/pkg/core/analytics/index.js.map +1 -1
  31. package/dist/pkg/core/http-cookies/index.js +171 -0
  32. package/dist/pkg/core/http-cookies/index.js.map +1 -0
  33. package/dist/pkg/core/user/index.js +32 -6
  34. package/dist/pkg/core/user/index.js.map +1 -1
  35. package/dist/pkg/core/user/tld.js +7 -3
  36. package/dist/pkg/core/user/tld.js.map +1 -1
  37. package/dist/pkg/generated/version.js +1 -1
  38. package/dist/pkg/index.js +1 -0
  39. package/dist/pkg/index.js.map +1 -1
  40. package/dist/pkg/node/index.js +1 -1
  41. package/dist/pkg/node/index.js.map +1 -1
  42. package/dist/pkg/plugins/analytics-node/index.js +2 -2
  43. package/dist/pkg/plugins/analytics-node/index.js.map +1 -1
  44. package/dist/pkg/plugins/destinations/destination.js +78 -0
  45. package/dist/pkg/plugins/destinations/destination.js.map +1 -0
  46. package/dist/pkg/plugins/destinations/google-tag-manager.js +45 -0
  47. package/dist/pkg/plugins/destinations/google-tag-manager.js.map +1 -0
  48. package/dist/pkg/plugins/destinations/index.js +18 -0
  49. package/dist/pkg/plugins/destinations/index.js.map +1 -0
  50. package/dist/pkg/plugins/destinations/types.js +2 -0
  51. package/dist/pkg/plugins/destinations/types.js.map +1 -0
  52. package/dist/types/browser/index.d.ts.map +1 -1
  53. package/dist/types/core/analytics/index.d.ts +14 -0
  54. package/dist/types/core/analytics/index.d.ts.map +1 -1
  55. package/dist/types/core/buffer/index.d.ts +1 -1
  56. package/dist/types/core/http-cookies/index.d.ts +57 -0
  57. package/dist/types/core/http-cookies/index.d.ts.map +1 -0
  58. package/dist/types/core/user/index.d.ts +5 -0
  59. package/dist/types/core/user/index.d.ts.map +1 -1
  60. package/dist/types/core/user/tld.d.ts.map +1 -1
  61. package/dist/types/generated/version.d.ts +1 -1
  62. package/dist/types/index.d.ts +1 -0
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/types/plugins/destinations/destination.d.ts +32 -0
  65. package/dist/types/plugins/destinations/destination.d.ts.map +1 -0
  66. package/dist/types/plugins/destinations/google-tag-manager.d.ts +30 -0
  67. package/dist/types/plugins/destinations/google-tag-manager.d.ts.map +1 -0
  68. package/dist/types/plugins/destinations/index.d.ts +6 -0
  69. package/dist/types/plugins/destinations/index.d.ts.map +1 -0
  70. package/dist/types/plugins/destinations/types.d.ts +5 -0
  71. package/dist/types/plugins/destinations/types.d.ts.map +1 -0
  72. package/dist/umd/events.min.js +1 -1
  73. package/dist/umd/events.min.js.map +1 -1
  74. package/dist/umd/google-tag-manager.bundle.c27daab560c3298c04ca.js +2 -0
  75. package/dist/umd/google-tag-manager.bundle.c27daab560c3298c04ca.js.map +1 -0
  76. package/dist/umd/index.js +1 -1
  77. package/dist/umd/index.js.map +1 -1
  78. package/package.json +14 -19
  79. package/src/browser/index.ts +20 -0
  80. package/src/core/analytics/index.ts +20 -0
  81. package/src/core/http-cookies/README.md +97 -0
  82. package/src/core/http-cookies/index.ts +165 -0
  83. package/src/core/http-cookies/server-examples/node-aws-lambda.md +167 -0
  84. package/src/core/http-cookies/server-examples/node-express-js.md +103 -0
  85. package/src/core/http-cookies/server-examples/node-next-js.md +75 -0
  86. package/src/core/user/index.ts +42 -4
  87. package/src/core/user/tld.ts +8 -4
  88. package/src/generated/version.ts +1 -1
  89. package/src/index.ts +1 -0
  90. package/src/node/index.ts +1 -1
  91. package/src/plugins/analytics-node/index.ts +2 -2
  92. package/src/plugins/destinations/destination.ts +85 -0
  93. package/src/plugins/destinations/google-tag-manager.ts +96 -0
  94. package/src/plugins/destinations/index.ts +19 -0
  95. package/src/plugins/destinations/types.ts +8 -0
@@ -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.
@@ -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
@@ -134,6 +140,14 @@ export class User {
134
140
  legacyUser.id && this.id(legacyUser.id)
135
141
  legacyUser.traits && this.traits(legacyUser.traits)
136
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
+
137
151
  autoBind(this)
138
152
  }
139
153
 
@@ -145,11 +159,22 @@ export class User {
145
159
  const prevId = this.identityStore.getAndSync(this.idKey)
146
160
 
147
161
  if (id !== undefined) {
162
+ const clearingIdentity = id === null
163
+ const changingIdentity = id !== prevId && prevId !== null && id !== null
164
+ const creatingIdentity = id !== prevId && prevId === null && id !== null
165
+
148
166
  this.identityStore.set(this.idKey, id)
149
167
 
150
- const changingIdentity = id !== prevId && prevId !== null && id !== null
168
+ if (clearingIdentity) {
169
+ this.options?.httpCookieService?.dispatchClear()
170
+ }
171
+
151
172
  if (changingIdentity) {
152
- this.anonymousId(null)
173
+ this.anonymousId(null) // this also runs dispatchClear()
174
+ }
175
+
176
+ if (changingIdentity || creatingIdentity) {
177
+ this.options?.httpCookieService?.dispatchCreate()
153
178
  }
154
179
  }
155
180
 
@@ -176,28 +201,36 @@ export class User {
176
201
 
177
202
  if (id === undefined) {
178
203
  let val = this.identityStore.getAndSync(this.anonKey)
204
+ let migrated = false
179
205
 
180
206
  // support anonymousId migration from other analytics providers
181
207
  if (!val) {
182
208
  val = decryptRudderHtValue(
183
209
  this.identityStore.getAndSync(rudderHtAnonymousIdKey) ?? ''
184
210
  )
211
+ migrated = Boolean(val)
185
212
  if (val) this.identityStore.set(this.anonKey, val)
186
213
  }
187
214
  if (!val) {
188
215
  val = this.identityStore.getAndSync(segmentAnonymousIdKey)
216
+ migrated = Boolean(val)
189
217
  if (val) this.identityStore.set(this.anonKey, val)
190
218
  }
191
219
  if (!val) {
192
220
  val = decryptRudderValue(
193
221
  this.identityStore.getAndSync(rudderAnonymousIdKey) ?? ''
194
222
  )
223
+ migrated = Boolean(val)
195
224
  if (val) this.identityStore.set(this.anonKey, val)
196
225
  }
197
226
  if (!val) {
198
227
  val = this.legacySIO()?.[0] ?? null
199
228
  }
200
229
 
230
+ if (migrated) {
231
+ this.options?.httpCookieService?.dispatchCreate()
232
+ }
233
+
201
234
  if (val) {
202
235
  return val
203
236
  }
@@ -205,11 +238,16 @@ export class User {
205
238
 
206
239
  if (id === null) {
207
240
  this.identityStore.set(this.anonKey, null)
208
- return this.identityStore.getAndSync(this.anonKey)
241
+ const clearedVal = this.identityStore.getAndSync(this.anonKey)
242
+ this.options?.httpCookieService?.dispatchClear()
243
+ return clearedVal
209
244
  }
210
245
 
211
246
  this.identityStore.set(this.anonKey, id ?? uuid())
212
- return this.identityStore.getAndSync(this.anonKey)
247
+ const syncedVal = this.identityStore.getAndSync(this.anonKey)
248
+
249
+ this.options?.httpCookieService?.dispatchCreate()
250
+ return syncedVal
213
251
  }
214
252
 
215
253
  traits = (traits?: Traits | null): Traits | undefined => {
@@ -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
- // Lookup the real top level one.
48
+ // Test for the top most domain that the browser allows
49
49
  for (let i = 0; i < lvls.length; ++i) {
50
- const cname = '__tld__'
50
+ const cname = Math.round(Math.random() * 10_000).toString()
51
51
  const domain = lvls[i]
52
- const opts = { domain: '.' + domain }
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)
@@ -1,2 +1,2 @@
1
1
  // This file is generated.
2
- export const version = '1.1.0'
2
+ export const version = '1.3.0'
package/src/index.ts CHANGED
@@ -9,5 +9,6 @@ export * from './core/user'
9
9
 
10
10
  export type { HtEventsSnippet } from './browser/standalone-interface'
11
11
  export type { MiddlewareFunction } from './plugins/middleware'
12
+ export { Destination } from './plugins/destinations'
12
13
  export { getGlobalAnalytics } from './lib/global-analytics-helper'
13
14
  export { UniversalStorage, Store, StorageObject } from './core/storage'
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
  }
@@ -23,7 +23,7 @@ export async function post(
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
 
@@ -0,0 +1,85 @@
1
+ import type { DestinationPlugin, Plugin } from '../../core/plugin'
2
+ import { Context, ContextCancelation } from '../../core/context'
3
+ import {
4
+ applyDestinationMiddleware,
5
+ DestinationMiddlewareFunction,
6
+ } from '../middleware'
7
+
8
+ // removes the return type from a function
9
+ type NoReturn<T> = T extends (...args: any[]) => any
10
+ ? (...args: Parameters<T>) => void | Promise<void>
11
+ : T
12
+
13
+ type PluginActions = Pick<
14
+ Plugin,
15
+ 'alias' | 'group' | 'identify' | 'page' | 'screen' | 'track'
16
+ >
17
+
18
+ type DestinationActions = {
19
+ [K in keyof PluginActions]: NoReturn<PluginActions[K]>
20
+ }
21
+
22
+ /**
23
+ * Convenience class for writing 3rd party destination plugins
24
+ */
25
+ export class Destination implements DestinationPlugin {
26
+ readonly type = 'destination'
27
+ readonly middleware: DestinationMiddlewareFunction[] = []
28
+
29
+ constructor(
30
+ readonly name: string,
31
+ readonly version: string,
32
+ readonly actions: DestinationActions
33
+ ) {}
34
+
35
+ isLoaded() {
36
+ return true
37
+ }
38
+
39
+ load() {
40
+ console.debug(`loaded destination plugin: ${this.name} v${this.version}`)
41
+ return Promise.resolve()
42
+ }
43
+
44
+ addMiddleware(...fn: DestinationMiddlewareFunction[]) {
45
+ this.middleware.push(...fn)
46
+ }
47
+
48
+ alias = this._createMethod('alias')
49
+ group = this._createMethod('group')
50
+ identify = this._createMethod('identify')
51
+ page = this._createMethod('page')
52
+ screen = this._createMethod('screen')
53
+ track = this._createMethod('track')
54
+
55
+ private async transform(ctx: Context): Promise<Context> {
56
+ const modifiedEvent = await applyDestinationMiddleware(
57
+ this.name,
58
+ ctx.event,
59
+ this.middleware
60
+ )
61
+
62
+ if (modifiedEvent == null) {
63
+ ctx.cancel(
64
+ new ContextCancelation({
65
+ retry: false,
66
+ reason: 'dropped by destination middleware',
67
+ })
68
+ )
69
+ }
70
+
71
+ return new Context(modifiedEvent)
72
+ }
73
+
74
+ private _createMethod(action: keyof DestinationActions) {
75
+ return async (ctx: Context): Promise<Context> => {
76
+ if (!this.actions[action]) return ctx
77
+
78
+ const transformedCtx = await this.transform(ctx)
79
+
80
+ await this.actions[action]!(transformedCtx)
81
+
82
+ return ctx
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,96 @@
1
+ import type { Context } from '../../core/context'
2
+ import type { DestinationFactory } from './types'
3
+ import { Destination } from './destination'
4
+
5
+ declare global {
6
+ interface Window {
7
+ gtag: Function
8
+ }
9
+ }
10
+
11
+ type GoogleTagManagerSettings = {
12
+ /**
13
+ * The Google measurement ID(s) to send events to (GA4, Ads)
14
+ */
15
+ measurementId?: string | string[]
16
+
17
+ /**
18
+ * If a `Viewed Page` event should be sent for all `htevents.page` calls
19
+ */
20
+ trackAllPages?: boolean
21
+
22
+ /**
23
+ * If a `Viewed <name> Page` event should be sent for `htevents.page('Name')` calls
24
+ */
25
+ trackNamedPages?: boolean
26
+
27
+ /**
28
+ * If a `Viewed <category> <name> Page` event should be sent for `htevents.page('Category', 'Name')` calls
29
+ */
30
+ trackCategorizedPages?: boolean
31
+ }
32
+
33
+ /**
34
+ * https://github.com/segmentio/analytics.js-integrations/blob/master/integrations/google-tag-manager/lib/index.js
35
+ */
36
+ const googleTagManager: DestinationFactory<GoogleTagManagerSettings> = ({
37
+ measurementId = [],
38
+ trackAllPages = false,
39
+ trackNamedPages = true,
40
+ trackCategorizedPages = true,
41
+ }) => {
42
+ const measurementIds = (
43
+ Array.isArray(measurementId) ? measurementId : [measurementId]
44
+ ).filter(Boolean)
45
+
46
+ const baseEvent = ({ event }: Context) => {
47
+ return {
48
+ ...(event.userId && {
49
+ user_id: event.userId,
50
+ }),
51
+ ...(event.anonymousId && {
52
+ hightouch_anonymous_id: event.anonymousId,
53
+ }),
54
+ ...(measurementIds.length > 0 && {
55
+ send_to: measurementIds,
56
+ }),
57
+ ...event.properties,
58
+ }
59
+ }
60
+
61
+ return new Destination('Google Tag Manager', '0.0.1', {
62
+ identify: (ctx) => {
63
+ if (ctx.event.userId) {
64
+ measurementIds.forEach((measurementId) => {
65
+ window.gtag('config', measurementId, {
66
+ user_id: ctx.event.userId,
67
+ })
68
+ })
69
+ }
70
+ },
71
+
72
+ page: (ctx) => {
73
+ if (
74
+ trackAllPages ||
75
+ (trackNamedPages && ctx.event.name) ||
76
+ (trackCategorizedPages && ctx.event.category)
77
+ ) {
78
+ const eventName = ['Viewed', ctx.event.category, ctx.event.name, 'Page']
79
+ .filter(Boolean)
80
+ .join(' ')
81
+
82
+ window.gtag('event', eventName, {
83
+ ...baseEvent(ctx),
84
+ })
85
+ }
86
+ },
87
+
88
+ track: (ctx) => {
89
+ window.gtag('event', ctx.event.event, {
90
+ ...baseEvent(ctx),
91
+ })
92
+ },
93
+ })
94
+ }
95
+
96
+ export default googleTagManager
@@ -0,0 +1,19 @@
1
+ import type { DestinationSettings } from './types'
2
+ import { Destination } from './destination'
3
+
4
+ export { Destination }
5
+ export type { DestinationSettings }
6
+
7
+ export async function createDestination(
8
+ name: string,
9
+ settings: DestinationSettings
10
+ ): Promise<Destination | undefined> {
11
+ switch (name) {
12
+ case 'Google Tag Manager':
13
+ return import(
14
+ /* webpackChunkName: "google-tag-manager" */ './google-tag-manager'
15
+ ).then((mod) => mod.default(settings as any))
16
+ default:
17
+ return undefined
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import type { JSONObject } from '@ht-sdks/events-sdk-js-core'
2
+ import type { Destination } from './destination'
3
+
4
+ export type DestinationSettings = JSONObject
5
+
6
+ export type DestinationFactory<TSettings extends DestinationSettings> = (
7
+ settings: TSettings
8
+ ) => Destination