@crimson-education/browser-logger 5.0.1-beta.2 → 5.0.1-beta.4

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 (65) hide show
  1. package/README.md +63 -85
  2. package/lib/index.d.ts +1 -1
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/index.js +46 -22
  5. package/lib/index.js.map +1 -1
  6. package/lib/logger/consoleTransport.d.ts +1 -1
  7. package/lib/logger/consoleTransport.d.ts.map +1 -1
  8. package/lib/logger/consoleTransport.js +22 -16
  9. package/lib/logger/consoleTransport.js.map +1 -1
  10. package/lib/logger/datadogTransport.d.ts +1 -1
  11. package/lib/logger/datadogTransport.d.ts.map +1 -1
  12. package/lib/logger/datadogTransport.js +8 -4
  13. package/lib/logger/datadogTransport.js.map +1 -1
  14. package/lib/logger/index.js +49 -29
  15. package/lib/logger/index.js.map +1 -1
  16. package/lib/logger/index.test.js +18 -16
  17. package/lib/logger/index.test.js.map +1 -1
  18. package/lib/logger/utils.js +9 -4
  19. package/lib/logger/utils.js.map +1 -1
  20. package/lib/reporters/amplifyReporter.d.ts +4 -4
  21. package/lib/reporters/amplifyReporter.d.ts.map +1 -1
  22. package/lib/reporters/amplifyReporter.js +133 -230
  23. package/lib/reporters/amplifyReporter.js.map +1 -1
  24. package/lib/reporters/amplifyReporter.test.js +12 -9
  25. package/lib/reporters/amplifyReporter.test.js.map +1 -1
  26. package/lib/reporters/datadogReporter.d.ts +1 -13
  27. package/lib/reporters/datadogReporter.d.ts.map +1 -1
  28. package/lib/reporters/datadogReporter.js +49 -122
  29. package/lib/reporters/datadogReporter.js.map +1 -1
  30. package/lib/reporters/gtmReporter.d.ts +1 -1
  31. package/lib/reporters/gtmReporter.d.ts.map +1 -1
  32. package/lib/reporters/gtmReporter.js +5 -1
  33. package/lib/reporters/gtmReporter.js.map +1 -1
  34. package/lib/reporters/index.js +71 -46
  35. package/lib/reporters/index.js.map +1 -1
  36. package/lib/reporters/logReporter.js +34 -24
  37. package/lib/reporters/logReporter.js.map +1 -1
  38. package/lib/reporters/posthogReporter.d.ts +51 -0
  39. package/lib/reporters/posthogReporter.d.ts.map +1 -0
  40. package/lib/reporters/posthogReporter.js +254 -0
  41. package/lib/reporters/posthogReporter.js.map +1 -0
  42. package/lib/reporters/posthogReporter.test.d.ts +2 -0
  43. package/lib/reporters/posthogReporter.test.d.ts.map +1 -0
  44. package/lib/reporters/posthogReporter.test.js +197 -0
  45. package/lib/reporters/posthogReporter.test.js.map +1 -0
  46. package/lib/types/index.js +18 -2
  47. package/lib/types/index.js.map +1 -1
  48. package/lib/types/logger.d.ts +3 -3
  49. package/lib/types/logger.d.ts.map +1 -1
  50. package/lib/types/logger.js +5 -2
  51. package/lib/types/logger.js.map +1 -1
  52. package/lib/types/reporter.d.ts +14 -6
  53. package/lib/types/reporter.d.ts.map +1 -1
  54. package/lib/types/reporter.js +2 -1
  55. package/lib/utils.js +5 -1
  56. package/lib/utils.js.map +1 -1
  57. package/lib/utils.test.js +4 -2
  58. package/lib/utils.test.js.map +1 -1
  59. package/package.json +17 -15
  60. package/src/index.ts +6 -0
  61. package/src/reporters/amplifyReporter.ts +140 -264
  62. package/src/reporters/datadogReporter.ts +22 -133
  63. package/src/reporters/posthogReporter.test.ts +244 -0
  64. package/src/reporters/posthogReporter.ts +374 -0
  65. package/src/types/reporter.ts +9 -0
@@ -1,7 +1,8 @@
1
- import { identifyUser, record } from '@aws-amplify/analytics';
2
- // import { Endpoint, FinalizeRequestMiddleware } from '@aws-sdk/types';
3
- // import { HttpRequest } from '@aws-sdk/protocol-http';
4
- // import { PinpointClient, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-pinpoint';
1
+ import { Auth } from '@aws-amplify/auth';
2
+ import { Analytics } from '@aws-amplify/analytics';
3
+ import { Endpoint, FinalizeRequestMiddleware } from '@aws-sdk/types';
4
+ import { HttpRequest } from '@aws-sdk/protocol-http';
5
+ import { PinpointClient, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-pinpoint';
5
6
 
6
7
  import {
7
8
  IReporter,
@@ -13,164 +14,12 @@ import {
13
14
  ServiceInfo,
14
15
  } from '../types';
15
16
  import { logger } from '../logger';
16
- // Note: fetchAuthSession was previously imported but unused; removing to satisfy linter
17
- import { Amplify } from '@aws-amplify/core';
18
17
 
19
18
  /* eslint-disable @typescript-eslint/no-unused-vars */
20
19
  type AttributeMap = Record<string, string[] | string | null>;
21
20
 
22
21
  type AmplifyAutoTrackSource = 'pageView' | 'event' | 'session';
23
22
 
24
- // Auto-tracking implementation for Gen2
25
- class AmplifyAutoTracker {
26
- private config: AmplifyReporterConfig;
27
- private allMetadata: AttributeMap;
28
- private currentUser: ReportUser | null = null;
29
- private sessionStartTime: number = Date.now();
30
- private pageViewCount: number = 0;
31
- private lastPageUrl: string = window.location.href;
32
- private isPageVisible: boolean = !document.hidden;
33
-
34
- constructor(config: AmplifyReporterConfig, allMetadata: AttributeMap) {
35
- this.config = config;
36
- this.allMetadata = allMetadata;
37
- this.setupEventListeners();
38
- }
39
-
40
- private setupEventListeners() {
41
- // Page visibility tracking
42
- document.addEventListener('visibilitychange', () => {
43
- this.isPageVisible = !document.hidden;
44
- if (this.config.autoTrackSessions) {
45
- this.trackSessionState();
46
- }
47
- });
48
-
49
- // Page view tracking
50
- if (this.config.autoTrackPageViews) {
51
- // Delay initial page view to ensure Amplify is fully configured
52
- // Using setTimeout(0) to defer to the next event loop tick
53
- setTimeout(() => {
54
- this.trackPageView();
55
- }, 0);
56
-
57
- // Track route changes for SPAs
58
- let currentUrl = window.location.href;
59
- const observer = new MutationObserver(() => {
60
- if (window.location.href !== currentUrl) {
61
- currentUrl = window.location.href;
62
- this.trackPageView();
63
- }
64
- });
65
- observer.observe(document.body, { childList: true, subtree: true });
66
- }
67
-
68
- // User interaction tracking
69
- if (this.config.autoTrackEvents) {
70
- this.setupInteractionTracking();
71
- }
72
- }
73
-
74
- private trackPageView() {
75
- this.pageViewCount++;
76
- const pageViewEvent = {
77
- message: 'Page View',
78
- metadata: {
79
- url: window.location.href,
80
- title: document.title,
81
- referrer: document.referrer,
82
- pageViewCount: this.pageViewCount,
83
- ...this.getAutoTrackMetadata('pageView'),
84
- },
85
- };
86
-
87
- record({
88
- name: pageViewEvent.message,
89
- attributes: asAttributeMap(
90
- {
91
- ...this.allMetadata,
92
- ...pageViewEvent.metadata,
93
- },
94
- false,
95
- ) as Record<string, string>,
96
- });
97
- }
98
-
99
- private trackSessionState() {
100
- const sessionDuration = Date.now() - this.sessionStartTime;
101
- const sessionEvent = {
102
- message: this.isPageVisible ? 'Session Active' : 'Session Inactive',
103
- metadata: {
104
- sessionDuration,
105
- isVisible: this.isPageVisible,
106
- ...this.getAutoTrackMetadata('session'),
107
- },
108
- };
109
-
110
- record({
111
- name: sessionEvent.message,
112
- attributes: asAttributeMap(
113
- {
114
- ...this.allMetadata,
115
- ...sessionEvent.metadata,
116
- },
117
- false,
118
- ) as Record<string, string>,
119
- });
120
- }
121
-
122
- private setupInteractionTracking() {
123
- const selectorPrefix = this.config.selectorPrefix ?? 'data-analytics-';
124
-
125
- document.addEventListener('click', (event) => {
126
- const target = event.target as HTMLElement;
127
- if (!target) return;
128
-
129
- // Find the closest element with analytics attributes
130
- const analyticsElement = target.closest(`[${selectorPrefix}name]`);
131
- if (!analyticsElement) return;
132
-
133
- const analyticsName = analyticsElement.getAttribute(`${selectorPrefix}name`);
134
- if (!analyticsName) return;
135
-
136
- const interactionEvent = {
137
- message: 'User Interaction',
138
- metadata: {
139
- elementName: analyticsName,
140
- elementType: target.tagName.toLowerCase(),
141
- elementText: target.textContent?.slice(0, 100),
142
- interactionType: 'click',
143
- ...this.getAutoTrackMetadata('event'),
144
- },
145
- };
146
-
147
- record({
148
- name: interactionEvent.message,
149
- attributes: asAttributeMap(
150
- {
151
- ...this.allMetadata,
152
- ...interactionEvent.metadata,
153
- },
154
- false,
155
- ) as Record<string, string>,
156
- });
157
- });
158
- }
159
-
160
- private getAutoTrackMetadata(source: AmplifyAutoTrackSource): Metadata {
161
- const { beforeAutoTrack } = this.config;
162
- return typeof beforeAutoTrack === 'function' ? (beforeAutoTrack(source) ?? {}) : {};
163
- }
164
-
165
- public setUser(user: ReportUser | null) {
166
- this.currentUser = user;
167
- }
168
-
169
- public updateMetadata(metadata: AttributeMap) {
170
- Object.assign(this.allMetadata, metadata);
171
- }
172
- }
173
-
174
23
  export interface AmplifyReporterConfig extends ReporterConfigBase {
175
24
  /**
176
25
  * AWS Region for Amplify.
@@ -180,7 +29,7 @@ export interface AmplifyReporterConfig extends ReporterConfigBase {
180
29
  * The Identity Pool Id to use for reporting, if set to false, Auth.configure is not called.
181
30
  * This must be called manually for the reporter to work.
182
31
  */
183
- identityPoolId: string;
32
+ identityPoolId: string | false;
184
33
  /**
185
34
  * The Pinpoint App Id to report to.
186
35
  */
@@ -247,21 +96,19 @@ type AmplifyReporterBufferingConfig = {
247
96
  };
248
97
 
249
98
  export function amplifyReporter(info: ServiceInfo, config: AmplifyReporterConfig): IReporter {
250
- Amplify.configure({
251
- Auth: {
252
- Cognito: {
253
- identityPoolId: config.identityPoolId,
254
- allowGuestAccess: true,
255
- },
256
- },
257
- Analytics: {
258
- Pinpoint: {
259
- appId: config.analyticsAppId,
260
- region: config.region,
261
- ...config.buffering,
262
- },
263
- },
264
- });
99
+ if (config.identityPoolId !== false) {
100
+ Auth.configure({
101
+ region: config.region,
102
+ identityPoolId: config.identityPoolId,
103
+ userPoolId: config.userPoolId,
104
+ userPoolWebClientId: config.userPoolWebClientId,
105
+ });
106
+ }
107
+
108
+ const wrapAutoTrackMiddleware = (source: AmplifyAutoTrackSource) => {
109
+ const { beforeAutoTrack } = config;
110
+ return typeof beforeAutoTrack === 'function' ? () => beforeAutoTrack(source) ?? {} : undefined;
111
+ };
265
112
 
266
113
  const allMetadata = asAttributeMap({
267
114
  appName: info.service,
@@ -271,26 +118,59 @@ export function amplifyReporter(info: ServiceInfo, config: AmplifyReporterConfig
271
118
  version: info.version,
272
119
  });
273
120
 
274
- // Initialize auto-tracker for Gen2
275
- const autoTracker = new AmplifyAutoTracker(config, allMetadata);
121
+ Analytics.configure({
122
+ region: config.region,
123
+ appId: config.analyticsAppId,
124
+ endpoint: {
125
+ attributes: allMetadata,
126
+ },
127
+ ...config.buffering,
128
+ });
129
+
130
+ // Session autotracking is enabled by default for backwards compatibility reasons, so we _must_
131
+ // call this unconditionally to ensure we opt out of session tracking when `autoTrackSessions` isn't set
132
+ // See: https://docs.amplify.aws/lib/analytics/autotrack/q/platform/js/#session-tracking
133
+ Analytics.autoTrack('session', {
134
+ enable: config.autoTrackSessions === true,
135
+ attributes: wrapAutoTrackMiddleware('session'),
136
+ });
137
+
138
+ if (config.autoTrackPageViews) {
139
+ Analytics.autoTrack('pageView', {
140
+ enable: true,
141
+ eventName: 'pageView',
142
+ type: 'SPA',
143
+ provider: 'AWSPinpoint',
144
+ attributes: wrapAutoTrackMiddleware('pageView'),
145
+ });
146
+ }
147
+
148
+ if (config.autoTrackEvents) {
149
+ Analytics.autoTrack('event', {
150
+ enable: true,
151
+ selectorPrefix: config.selectorPrefix ?? 'data-analytics-',
152
+ attributes: wrapAutoTrackMiddleware('event'),
153
+ });
154
+ }
276
155
 
277
156
  if (config.proxyUrl) {
278
- // installPinpointProxy(new URL(config.proxyUrl));
157
+ installPinpointProxy(new URL(config.proxyUrl));
279
158
  }
280
159
 
281
160
  const reporter: IReporter = {
282
161
  trackEvent: function (event: ReporterEvent): void {
283
- record({
162
+ Analytics.record({
284
163
  name: event.message,
285
164
  attributes: asAttributeMap(
286
165
  {
287
- ...allMetadata,
288
166
  ...event.metadata,
289
167
  ...event.tags,
290
168
  },
291
169
  false,
292
170
  ) as Record<string, string>,
293
171
  metrics: event.metrics,
172
+ }).catch(() => {
173
+ // Swallow; see: https://crimsonhq.slack.com/archives/G4UN6Q4KF/p1648599302847539
294
174
  });
295
175
  },
296
176
  addBreadcrumb: function (breadcrumb: ReporterBreadcrumb): void {
@@ -304,9 +184,11 @@ export function amplifyReporter(info: ServiceInfo, config: AmplifyReporterConfig
304
184
  },
305
185
  addMetadata: function (metadata: Metadata): void {
306
186
  Object.assign(allMetadata, asAttributeMap(metadata, true));
307
- autoTracker.updateMetadata(asAttributeMap(metadata, true));
308
- // Note: updateEndpoint is not available in Amplify v7
309
- // Metadata updates would need to be handled differently
187
+ Analytics.updateEndpoint({
188
+ attributes: allMetadata,
189
+ }).catch(() => {
190
+ // Swallow; see: https://crimsonhq.slack.com/archives/G4UN6Q4KF/p1648599302847539
191
+ });
310
192
  },
311
193
  setUser: function (user: ReportUser | null): void {
312
194
  const userMetadata = user
@@ -315,21 +197,20 @@ export function amplifyReporter(info: ServiceInfo, config: AmplifyReporterConfig
315
197
  })
316
198
  : {};
317
199
  Object.assign(allMetadata, asAttributeMap(userMetadata, true));
318
- autoTracker.setUser(user);
319
- // Only call identifyUser when we have a valid user ID
320
- // Calling with empty string can cause issues with Pinpoint USER_ID
321
- if (user?.id) {
322
- identifyUser({
323
- userId: user.id,
324
- userProfile: {
325
- email: user.email,
326
- },
327
- }).catch((error) => {
328
- // Credentials may not be ready yet, log warning and continue
329
- // The userId is still stored in allMetadata for event tracking
330
- logger.warn('Failed to identify user in Pinpoint', { error });
331
- });
332
- }
200
+ Analytics.updateEndpoint({
201
+ userId: user?.id ?? '',
202
+ attributes: allMetadata,
203
+ userAttributes: user
204
+ ? asAttributeMap({
205
+ id: user.id,
206
+ email: user.email,
207
+ name: user.name ?? user.email,
208
+ username: user.username,
209
+ })
210
+ : {},
211
+ }).catch(() => {
212
+ // Swallow; see: https://crimsonhq.slack.com/archives/G4UN6Q4KF/p1648599302847539
213
+ });
333
214
  },
334
215
  setRouteName: function (routeName: string): void {
335
216
  reporter.addMetadata({ routeName });
@@ -351,8 +232,8 @@ export function asAttributeMap(values: Record<string, unknown>, groupValues = tr
351
232
  const checkedEntries = Object.entries(attributeMap).map(([key, value]) => {
352
233
  const truncatedKey = key.length > 50 ? `___${key.slice(-47)}` : key;
353
234
  const truncatedValue = Array.isArray(value)
354
- ? (value?.map((val) => val.slice(0, 100)) ?? null)
355
- : (value?.slice(0, 100) ?? null);
235
+ ? value?.map((val) => val.slice(0, 100)) ?? null
236
+ : value?.slice(0, 100) ?? null;
356
237
 
357
238
  return [truncatedKey, truncatedValue];
358
239
  });
@@ -383,13 +264,8 @@ export function buildAttributeMap(
383
264
  Object.entries(values).forEach(([key, value]) => {
384
265
  const combinedKey = parentKey ? [parentKey, key].join('.') : key;
385
266
 
386
- // Only treat undefined or null as empty; avoid converting falsy values like ''/0/false to null
387
- if (value === undefined || value === null) {
388
- // For event attributes (groupValues === false), Pinpoint rejects null values.
389
- // In that case we omit the key entirely to avoid "Event attribute value can not be null".
390
- if (groupValues) {
391
- valuesWithStringArrays[combinedKey] = null;
392
- }
267
+ if (!value) {
268
+ valuesWithStringArrays[combinedKey] = null;
393
269
  } else if (groupValues && Array.isArray(value)) {
394
270
  valuesWithStringArrays[combinedKey] = value.map((element) =>
395
271
  typeof element === 'string' ? element : JSON.stringify(element),
@@ -406,63 +282,63 @@ export function buildAttributeMap(
406
282
  return valuesWithStringArrays;
407
283
  }
408
284
 
409
- // function installPinpointProxy(proxyUrl: URL) {
410
- // // No public API for overriding where the Pinpoint client sends events to... 🤮
411
- // // In theory you can pass in an `endpoint` to the Pinpoint client's constructor like any other AWS
412
- // // client, but Amplify's analytics doesn't expose anything we can use to get an endpoint threaded
413
- // // down to the Pinpoint client's constructor.
414
- // //
415
- // // The Pinpoint client _also_ isn't available synchronously because it is instantiated when events
416
- // // get sent out, and then reconfigured whenever the API credentials change. We need to hook `_initClients`
417
- // // to ensure that the Pinpoint client being used is always patched with our custom endpoint.
418
- // const provider = (globalThis as any).Analytics?.getPluggable?.('AWSPinpoint') as any;
419
- // if (!provider || typeof provider._initClients !== 'function') {
420
- // logger.error(
421
- // 'Installation of the Pinpoint proxy failed. This likely means the internals of the @aws-amplify/analytics package have changed.',
422
- // );
423
- // return;
424
- // }
425
-
426
- // const originalInitClients = provider._initClients;
427
- // const requestMiddleware: FinalizeRequestMiddleware<ServiceInputTypes, ServiceOutputTypes> =
428
- // (next) => async (args) => {
429
- // const pinpointClient = provider.pinpointClient as PinpointClient | undefined;
430
-
431
- // if (pinpointClient && proxyUrl.pathname !== '/' && HttpRequest.isInstance(args.request)) {
432
- // // Add proxyUrl.pathname to final request url if it was provided
433
- // const shouldStripSlash = proxyUrl.pathname.endsWith('/');
434
- // args.request.path = `${proxyUrl.pathname}${args.request.path.slice(shouldStripSlash ? 1 : 0)}`;
435
-
436
- // // Wrap request body so the proxy has signing info
437
- // if (typeof args.request.body === 'string') {
438
- // const credentials = await pinpointClient.config.credentials();
439
- // args.request.body = JSON.stringify({
440
- // credentials,
441
- // data: JSON.parse(args.request.body),
442
- // });
443
- // }
444
- // }
445
-
446
- // return next(args);
447
- // };
448
-
449
- // provider._initClients = async (credentials: unknown) => {
450
- // const result = await originalInitClients.call(provider, credentials);
451
- // const pinpointClient = provider.pinpointClient as PinpointClient | undefined;
452
-
453
- // if (pinpointClient) {
454
- // pinpointClient.config.endpoint = (): Promise<Endpoint> =>
455
- // Promise.resolve({
456
- // hostname: proxyUrl.hostname,
457
- // // Passing proxyUrl.pathname here doesn't work; it gets overridden
458
- // path: '/',
459
- // port: undefined,
460
- // protocol: proxyUrl.protocol,
461
- // });
462
-
463
- // pinpointClient.middlewareStack.remove(requestMiddleware);
464
- // pinpointClient.middlewareStack.add(requestMiddleware, { step: 'finalizeRequest' });
465
- // }
466
- // return result;
467
- // };
468
- // }
285
+ function installPinpointProxy(proxyUrl: URL) {
286
+ // No public API for overriding where the Pinpoint client sends events to... 🤮
287
+ // In theory you can pass in an `endpoint` to the Pinpoint client's constructor like any other AWS
288
+ // client, but Amplify's analytics doesn't expose anything we can use to get an endpoint threaded
289
+ // down to the Pinpoint client's constructor.
290
+ //
291
+ // The Pinpoint client _also_ isn't available synchronously because it is instantiated when events
292
+ // get sent out, and then reconfigured whenever the API credentials change. We need to hook `_initClients`
293
+ // to ensure that the Pinpoint client being used is always patched with our custom endpoint.
294
+ const provider = Analytics.getPluggable('AWSPinpoint') as any;
295
+ if (!provider || typeof provider._initClients !== 'function') {
296
+ logger.error(
297
+ 'Installation of the Pinpoint proxy failed. This likely means the internals of the @aws-amplify/analytics package have changed.',
298
+ );
299
+ return;
300
+ }
301
+
302
+ const originalInitClients = provider._initClients;
303
+ const requestMiddleware: FinalizeRequestMiddleware<ServiceInputTypes, ServiceOutputTypes> =
304
+ (next) => async (args) => {
305
+ const pinpointClient = provider.pinpointClient as PinpointClient | undefined;
306
+
307
+ if (pinpointClient && proxyUrl.pathname !== '/' && HttpRequest.isInstance(args.request)) {
308
+ // Add proxyUrl.pathname to final request url if it was provided
309
+ const shouldStripSlash = proxyUrl.pathname.endsWith('/');
310
+ args.request.path = `${proxyUrl.pathname}${args.request.path.slice(shouldStripSlash ? 1 : 0)}`;
311
+
312
+ // Wrap request body so the proxy has signing info
313
+ if (typeof args.request.body === 'string') {
314
+ const credentials = await pinpointClient.config.credentials();
315
+ args.request.body = JSON.stringify({
316
+ credentials,
317
+ data: JSON.parse(args.request.body),
318
+ });
319
+ }
320
+ }
321
+
322
+ return next(args);
323
+ };
324
+
325
+ provider._initClients = async (credentials: unknown) => {
326
+ const result = await originalInitClients.call(provider, credentials);
327
+ const pinpointClient = provider.pinpointClient as PinpointClient | undefined;
328
+
329
+ if (pinpointClient) {
330
+ pinpointClient.config.endpoint = (): Promise<Endpoint> =>
331
+ Promise.resolve({
332
+ hostname: proxyUrl.hostname,
333
+ // Passing proxyUrl.pathname here doesn't work; it gets overridden
334
+ path: '/',
335
+ port: undefined,
336
+ protocol: proxyUrl.protocol,
337
+ });
338
+
339
+ pinpointClient.middlewareStack.remove(requestMiddleware);
340
+ pinpointClient.middlewareStack.add(requestMiddleware, { step: 'finalizeRequest' });
341
+ }
342
+ return result;
343
+ };
344
+ }