@crimson-education/browser-logger 2.0.0 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimson-education/browser-logger",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "An abstract logger and reporting utility for browser environments",
5
5
  "scripts": {
6
6
  "prepack": "npm run build",
@@ -44,7 +44,8 @@
44
44
  "typescript": "^4.1.2"
45
45
  },
46
46
  "files": [
47
- "lib"
47
+ "lib",
48
+ "src"
48
49
  ],
49
50
  "main": "lib/index.js"
50
51
  }
package/src/index.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { ILogger, Metadata, ReportError, ReportUser, ServiceInfo } from './types';
2
+ import { IReporter, ReporterBreadcrumb, ReporterEvent, TrackedReporterEvent } from './reporters';
3
+ import { consoleLogger } from './utils';
4
+ import { datadogLogger, datadogReporter, DatadogReporterConfig } from './reporters/datadogReporter';
5
+ import { gtmReporter } from './reporters/gtmReporter';
6
+ import { amplifyReporter, AmplifyReporterConfig } from './reporters/amplifyReporter';
7
+
8
+ export * from './types';
9
+
10
+ export let logger: ILogger | null = null;
11
+
12
+ export type ReporterConfig = ServiceInfo & {
13
+ // Datadog
14
+ datadog?: DatadogReporterConfig;
15
+
16
+ // Amplify/Pinpoint
17
+ amplify?: AmplifyReporterConfig;
18
+
19
+ // Google Tag Manager
20
+ gtm?: boolean;
21
+ };
22
+
23
+ const reporters: IReporter[] = [];
24
+ let initialized = false;
25
+ let ddInitialized = false;
26
+
27
+ export function init(config: ReporterConfig) {
28
+ initialized = true;
29
+
30
+ if (config.datadog) {
31
+ reporters.push(datadogReporter(config, config.datadog));
32
+ ddInitialized = true;
33
+ }
34
+
35
+ if (config.amplify) {
36
+ reporters.push(amplifyReporter(config, config.amplify));
37
+ }
38
+
39
+ if (config.gtm) {
40
+ reporters.push(gtmReporter());
41
+ }
42
+
43
+ if (config.defaultMetadata) {
44
+ for (const reporter of reporters) {
45
+ reporter.addMetadata(config.defaultMetadata);
46
+ }
47
+ }
48
+
49
+ logger = createLogger('Reporter');
50
+ }
51
+
52
+ export function trackEvent(event: ReporterEvent): void {
53
+ for (const reporter of reporters) {
54
+ reporter.trackEvent(event);
55
+ }
56
+ }
57
+
58
+ export function addBreadcrumb(breadcrumb: ReporterBreadcrumb): void {
59
+ for (const reporter of reporters) {
60
+ reporter.addBreadcrumb(breadcrumb);
61
+ }
62
+ }
63
+
64
+ export function addMetadata(metadata: Metadata): void {
65
+ for (const reporter of reporters) {
66
+ reporter.addMetadata(metadata);
67
+ }
68
+ }
69
+
70
+ export function setUser(user: ReportUser | null): void {
71
+ for (const reporter of reporters) {
72
+ reporter.setUser(user);
73
+ }
74
+ }
75
+
76
+ export function setRouteName(routeName: string): void {
77
+ for (const reporter of reporters) {
78
+ reporter.setRouteName(routeName);
79
+ }
80
+ }
81
+
82
+ export function setPageName(pageName: string): void {
83
+ for (const reporter of reporters) {
84
+ reporter.setPageName(pageName);
85
+ }
86
+ }
87
+
88
+ export function reportError(error: ReportError, metadata?: Metadata): void {
89
+ for (const reporter of reporters) {
90
+ reporter.reportError(error, metadata);
91
+ }
92
+ }
93
+
94
+ export function recordSession(): void {
95
+ for (const reporter of reporters) {
96
+ reporter.recordSession();
97
+ }
98
+ }
99
+
100
+ export function recordSessionStop(): void {
101
+ for (const reporter of reporters) {
102
+ reporter.recordSessionStop();
103
+ }
104
+ }
105
+
106
+ export function trackEventSinceLastAction(event: ReporterEvent): void {
107
+ const lastEvent = getLastTrackedEvent();
108
+ if (lastEvent) {
109
+ const duration = new Date().getTime() - lastEvent.occurred.getTime();
110
+ trackEvent({
111
+ ...event,
112
+ metadata: {
113
+ ...event.metadata,
114
+ lastEventName: lastEvent.message,
115
+ timeSinceLastEvent: duration,
116
+ },
117
+ });
118
+ } else {
119
+ trackEvent(event);
120
+ }
121
+ sessionStorage.setItem('loggerLastEvent', JSON.stringify({ ...event, occurred: new Date() }));
122
+ }
123
+
124
+ export function getLastTrackedEvent(): TrackedReporterEvent | null {
125
+ const eventStr = sessionStorage.getItem('loggerLastEvent');
126
+ if (!eventStr) return null;
127
+
128
+ const event: TrackedReporterEvent = JSON.parse(eventStr);
129
+ event.occurred = new Date(event.occurred);
130
+ return event;
131
+ }
132
+
133
+ export function createLogger(name?: string, options?: { metadata?: Metadata }): ILogger {
134
+ if (!initialized) {
135
+ throw new Error('You must call init on BrowserLogger before creating a logger');
136
+ }
137
+
138
+ if (ddInitialized) {
139
+ return datadogLogger(name, options);
140
+ } else {
141
+ return consoleLogger(name, options);
142
+ }
143
+ }
@@ -0,0 +1,74 @@
1
+ import { asAttributeMap, filterAttributeMap } from './amplifyReporter';
2
+
3
+ describe('amplifyReporter', () => {
4
+ it('should convert all attribute values to arrays of strings', () => {
5
+ const testMetadata = { a: 'a', d: ['e', 'f'] };
6
+ const attributeMap = asAttributeMap(testMetadata);
7
+
8
+ expect(attributeMap.a).toEqual(['a']);
9
+ expect(attributeMap.d).toEqual(['e', 'f']);
10
+ });
11
+
12
+ it('should handle undefined / null attributes', () => {
13
+ const testMetadata = { a: null, d: undefined };
14
+ const attributeMap = asAttributeMap(testMetadata);
15
+
16
+ expect(attributeMap.a).toBeNull();
17
+ expect(attributeMap.d).toBeNull();
18
+ });
19
+
20
+ it('should flatten hierarchies', () => {
21
+ const testMetadata = { foo: { bar: 'baz' } };
22
+ const attributeMap = asAttributeMap(testMetadata);
23
+
24
+ expect(attributeMap['foo.bar']).toEqual(['baz']);
25
+ });
26
+
27
+ it('should stringify non-string array members', () => {
28
+ const testMetadata = { foo: 5, bar: [{ baz: 'maz' }] };
29
+ const attributeMap = asAttributeMap(testMetadata);
30
+
31
+ expect(attributeMap.foo).toEqual(['5']);
32
+ expect(typeof attributeMap.bar?.[0]).toEqual('string');
33
+ });
34
+
35
+ it('should truncate attribute names', () => {
36
+ const testMetadata = { thisIsAVeryVeryLongAttributeNameThatNeedsToBeTruncated: 5 };
37
+ const attributeMap = asAttributeMap(testMetadata);
38
+
39
+ expect(attributeMap.___VeryVeryLongAttributeNameThatNeedsToBeTruncated).toEqual(['5']);
40
+ });
41
+
42
+ it('should truncate attribute values', () => {
43
+ const testMetadata = {
44
+ a: 'ThisIsAVeryLongStringThatNeedsToBeTruncatedTo100CharsOrElseThereWillBeAProblemWithSendingTheBeautifulDataToPinpoint',
45
+ };
46
+ const attributeMap = asAttributeMap(testMetadata);
47
+
48
+ expect(attributeMap.a).toEqual([
49
+ 'ThisIsAVeryLongStringThatNeedsToBeTruncatedTo100CharsOrElseThereWillBeAProblemWithSendingTheBeautifu',
50
+ ]);
51
+ });
52
+
53
+ it('should not group values into arrays when groupValues===false', () => {
54
+ const testMetadata = {
55
+ a: '5',
56
+ };
57
+
58
+ const attributeMap = asAttributeMap(testMetadata, false);
59
+ expect(attributeMap.a).toEqual('5');
60
+ });
61
+
62
+ it('should remove attributes which match the ignore patterns', () => {
63
+ const inputAttributeMap = {
64
+ includeme: '5',
65
+ excludeme: 'false',
66
+ differentProp: 'boo',
67
+ };
68
+
69
+ const filtered = filterAttributeMap(inputAttributeMap, [/exclude/g, /Prop/g]);
70
+ expect(filtered['includeme']).toBeTruthy();
71
+ expect(filtered['excludeme']).toBeFalsy();
72
+ expect(filtered['differentProp']).toBeFalsy();
73
+ });
74
+ });
@@ -0,0 +1,208 @@
1
+ import { IReporter, ReporterBreadcrumb, ReporterEvent } from '.';
2
+ import { Metadata, ReportUser, ServiceInfo } from '../types';
3
+ import { Auth } from '@aws-amplify/auth';
4
+ import { Analytics } from '@aws-amplify/analytics';
5
+
6
+ export type AmplifyReporterConfig = {
7
+ region: string;
8
+ identityPoolId: string;
9
+ analyticsAppId: string;
10
+ autoTrackPageViews?: boolean;
11
+ autoTrackEvents?: boolean;
12
+ autoTrackSessions?: boolean;
13
+ selectorPrefix?: string;
14
+ ignoreBreadcrumbCategories?: string[];
15
+ buffering?: AmplifyReporterBufferingConfig;
16
+ userPoolId?: string;
17
+ userPoolWebClientId?: string;
18
+ ignoreMetadataPatterns?: RegExp[];
19
+ };
20
+
21
+ /**
22
+ * Configuration options for the buffering behavior of Pinpoint's event tracker.
23
+ *
24
+ * @see https://docs.amplify.aws/lib/analytics/getting-started/q/platform/js/#set-up-existing-analytics-backend
25
+ */
26
+ type AmplifyReporterBufferingConfig = {
27
+ /** Number of items to buffer for sending. */
28
+ bufferSize?: number;
29
+ /** Number of events sent each time Pinpoint flushes. */
30
+ flushSize?: number;
31
+ /** Interval Pinpoint flushes analytics events. Measured in milliseconds. */
32
+ flushInterval?: number;
33
+ /** The maximum number of times Pinpoint will retry to send an event. */
34
+ resendLimit?: number;
35
+ };
36
+
37
+ export function amplifyReporter(info: ServiceInfo, config: AmplifyReporterConfig): IReporter {
38
+ Auth.configure({
39
+ region: config.region,
40
+ identityPoolId: config.identityPoolId,
41
+ userPoolId: config.userPoolId,
42
+ userPoolWebClientId: config.userPoolWebClientId,
43
+ });
44
+
45
+ const allMetadata = asAttributeMap({
46
+ appName: info.service,
47
+ service: info.service,
48
+ domain: window.location.host,
49
+ environment: info.service,
50
+ version: info.version,
51
+ });
52
+
53
+ Analytics.configure({
54
+ region: config.region,
55
+ appId: config.analyticsAppId,
56
+ autoSessionRecord: config.autoTrackSessions,
57
+ endpoint: {
58
+ attributes: allMetadata,
59
+ },
60
+ ...config.buffering,
61
+ });
62
+
63
+ if (config.autoTrackPageViews) {
64
+ Analytics.autoTrack('pageView', {
65
+ enable: true,
66
+ eventName: 'pageView',
67
+ type: 'SPA',
68
+ provider: 'AWSPinpoint',
69
+ });
70
+ }
71
+
72
+ if (config.autoTrackEvents) {
73
+ Analytics.autoTrack('event', {
74
+ enable: true,
75
+ selectorPrefix: config.selectorPrefix ?? 'data-analytics-',
76
+ });
77
+ }
78
+
79
+ const reporter: IReporter = {
80
+ trackEvent: function (event: ReporterEvent): void {
81
+ Analytics.record({
82
+ name: event.message,
83
+ attributes: asAttributeMap(
84
+ {
85
+ ...event.metadata,
86
+ ...event.tags,
87
+ },
88
+ false,
89
+ config.ignoreMetadataPatterns,
90
+ ) as Record<string, string>,
91
+ metrics: event.metrics,
92
+ });
93
+ },
94
+ addBreadcrumb: function (breadcrumb: ReporterBreadcrumb): void {
95
+ if (breadcrumb.category && config.ignoreBreadcrumbCategories?.includes(breadcrumb.category)) {
96
+ return;
97
+ }
98
+
99
+ reporter.trackEvent({
100
+ message: breadcrumb.message,
101
+ metadata: {
102
+ category: breadcrumb.category,
103
+ ...breadcrumb.metadata,
104
+ },
105
+ });
106
+ },
107
+ addMetadata: function (metadata: Metadata): void {
108
+ Object.assign(allMetadata, asAttributeMap(metadata, true, config.ignoreMetadataPatterns));
109
+ Analytics.updateEndpoint({
110
+ attributes: allMetadata,
111
+ }).catch(() => {
112
+ // Swallow; see: https://crimsonhq.slack.com/archives/G4UN6Q4KF/p1648599302847539
113
+ });
114
+ },
115
+ setUser: function (user: ReportUser | null): void {
116
+ Analytics.updateEndpoint({
117
+ userId: user?.id ?? '',
118
+ userAttributes: user
119
+ ? asAttributeMap({
120
+ id: user.id,
121
+ email: user.email,
122
+ name: user.name ?? user.email,
123
+ username: user.username,
124
+ })
125
+ : {},
126
+ }).catch(() => {
127
+ // Swallow; see: https://crimsonhq.slack.com/archives/G4UN6Q4KF/p1648599302847539
128
+ });
129
+ },
130
+ setRouteName: function (routeName: string): void {
131
+ reporter.addMetadata({ routeName });
132
+ },
133
+ setPageName: function (pageName: string): void {
134
+ reporter.addMetadata({ pageName });
135
+ },
136
+ reportError: function (): void {},
137
+ recordSession: function (): void {},
138
+ recordSessionStop: function (): void {},
139
+ };
140
+
141
+ return reporter;
142
+ }
143
+
144
+ type AttributeMap = Record<string, string[] | string | null>;
145
+
146
+ /**
147
+ * Pinpoint has strict attribute name and value length limits
148
+ */
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ export function asAttributeMap(
151
+ values: Record<string, any>,
152
+ groupValues = true,
153
+ ignorePatterns: RegExp[] = [],
154
+ ): AttributeMap {
155
+ const attributeMap = buildAttributeMap(values, undefined, groupValues);
156
+ const filteredAttributeMap = filterAttributeMap(attributeMap, ignorePatterns);
157
+
158
+ const checkedEntries = Object.entries(filteredAttributeMap).map(([key, value]) => {
159
+ const truncatedKey = key.length > 50 ? `___${key.slice(-47)}` : key;
160
+ const truncatedValue = Array.isArray(value)
161
+ ? value?.map((val) => val.slice(0, 100)) ?? null
162
+ : value?.slice(0, 100) ?? null;
163
+
164
+ return [truncatedKey, truncatedValue];
165
+ });
166
+
167
+ return Object.fromEntries(checkedEntries);
168
+ }
169
+
170
+ /**
171
+ * Pinpoint expects `endpoint.attributes` and `endpoint.userAttributes` to have
172
+ * values which are string arrays. This function takes in an object and ensures
173
+ * all of its values are of type `string[]` to appease Pinpoint.
174
+ */
175
+ export function buildAttributeMap(
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ values: Record<string, any>,
178
+ parentKey: string | undefined = undefined,
179
+ groupValues = true,
180
+ ): AttributeMap {
181
+ const valuesWithStringArrays: AttributeMap = {};
182
+
183
+ Object.entries(values).forEach(([key, value]) => {
184
+ const combinedKey = parentKey ? [parentKey, key].join('.') : key;
185
+
186
+ if (!value) {
187
+ valuesWithStringArrays[combinedKey] = null;
188
+ } else if (groupValues && Array.isArray(value)) {
189
+ valuesWithStringArrays[combinedKey] = value.map((element) =>
190
+ typeof element === 'string' ? element : JSON.stringify(element),
191
+ );
192
+ } else if (typeof value === 'object') {
193
+ const flattenedAttribute = buildAttributeMap(value, combinedKey, groupValues);
194
+ Object.assign(valuesWithStringArrays, flattenedAttribute);
195
+ } else {
196
+ const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
197
+ valuesWithStringArrays[combinedKey] = groupValues ? [stringValue] : stringValue;
198
+ }
199
+ });
200
+
201
+ return valuesWithStringArrays;
202
+ }
203
+
204
+ export function filterAttributeMap(attributes: AttributeMap | Record<string, string>, ignorePatterns: RegExp[]) {
205
+ const entries = Object.entries(attributes);
206
+
207
+ return Object.fromEntries(entries.filter(([key]) => !ignorePatterns.some((pattern) => pattern.test(key))));
208
+ }
@@ -0,0 +1,208 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { IReporter, ReporterBreadcrumb, ReporterEvent } from '.';
3
+ import { Metadata, Profiler, ReportError, ReportUser, ServiceInfo } from '../types';
4
+ import { datadogLogs, HandlerType, StatusType } from '@datadog/browser-logs';
5
+ import { datadogRum, DefaultPrivacyLevel } from '@datadog/browser-rum';
6
+ import { ILogger, LogLevel } from '..';
7
+
8
+ export type DatadogReporterConfig = {
9
+ applicationId: string;
10
+ clientToken: string;
11
+ version?: string;
12
+ site?: string;
13
+ proxyUrl?: string;
14
+ forwardConsoleLogs?: boolean;
15
+ trackInteractions?: boolean;
16
+ sampleRate?: number;
17
+ replaySampleRate?: number;
18
+ defaultPrivacyLevel?: DefaultPrivacyLevel;
19
+ useSecureSessionCookie?: boolean;
20
+ useCrossSiteSessionCookie?: boolean;
21
+ allowedTracingOrigins?: (string | RegExp)[];
22
+ trackSessionAcrossSubdomains?: boolean;
23
+ actionNameAttribute?: string;
24
+ trackViewsManually?: boolean;
25
+ };
26
+
27
+ function getDatadogStatusType(level: LogLevel): StatusType {
28
+ switch (level) {
29
+ case LogLevel.Error:
30
+ return StatusType.error;
31
+
32
+ case LogLevel.Warn:
33
+ return StatusType.warn;
34
+
35
+ case LogLevel.Info:
36
+ return StatusType.info;
37
+
38
+ case LogLevel.Debug:
39
+ default:
40
+ return StatusType.debug;
41
+ }
42
+ }
43
+
44
+ export function datadogReporter(info: ServiceInfo, config: DatadogReporterConfig): IReporter {
45
+ const isLocalhost = window.location.hostname === 'localhost';
46
+
47
+ datadogLogs.init({
48
+ site: config.site,
49
+ proxyUrl: config.proxyUrl,
50
+ clientToken: config.clientToken,
51
+ service: info.service,
52
+ env: info.environment,
53
+ version: config.version ?? info.version,
54
+
55
+ useSecureSessionCookie: config.useSecureSessionCookie ?? !isLocalhost,
56
+ useCrossSiteSessionCookie: config.useCrossSiteSessionCookie ?? false,
57
+ trackSessionAcrossSubdomains: config.trackSessionAcrossSubdomains,
58
+
59
+ forwardErrorsToLogs: config.forwardConsoleLogs ?? false,
60
+ });
61
+
62
+ datadogRum.init({
63
+ site: config.site,
64
+ proxyUrl: config.proxyUrl,
65
+ clientToken: config.clientToken,
66
+ applicationId: config.applicationId,
67
+ service: info.service,
68
+ env: info.environment,
69
+ version: config.version ?? info.version,
70
+
71
+ useSecureSessionCookie: config.useSecureSessionCookie ?? !isLocalhost,
72
+ useCrossSiteSessionCookie: config.useCrossSiteSessionCookie ?? false,
73
+ trackSessionAcrossSubdomains: config.trackSessionAcrossSubdomains,
74
+
75
+ // Set sampleRate to 100 to capture 100%
76
+ // of transactions for performance monitoring.
77
+ sampleRate: config.sampleRate ?? 100,
78
+ replaySampleRate: config.replaySampleRate ?? 100,
79
+ trackInteractions: config.trackInteractions ?? false,
80
+ defaultPrivacyLevel: config.defaultPrivacyLevel ?? 'mask-user-input',
81
+ allowedTracingOrigins: config.allowedTracingOrigins,
82
+ actionNameAttribute: config.actionNameAttribute ?? 'data-analytics-name',
83
+ trackViewsManually: config.trackViewsManually ?? false,
84
+ });
85
+
86
+ const reporter: IReporter = {
87
+ trackEvent: function (event: ReporterEvent): void {
88
+ const context = {
89
+ level: event.level,
90
+ ...event.metadata,
91
+ ...event.tags,
92
+ ...event.metrics,
93
+ };
94
+ datadogRum.addAction(event.message, context);
95
+ datadogLogs.logger?.log(event.message, context, getDatadogStatusType(event.level ?? LogLevel.Info));
96
+ },
97
+ addBreadcrumb: function (breadcrumb: ReporterBreadcrumb): void {
98
+ const context = {
99
+ ...breadcrumb.metadata,
100
+ category: breadcrumb.category,
101
+ };
102
+ datadogRum.addAction(breadcrumb.message, context);
103
+ },
104
+ addMetadata: function (metadata: Metadata): void {
105
+ for (const [key, value] of Object.entries(metadata)) {
106
+ if (value !== null) {
107
+ datadogRum.addRumGlobalContext(key, value);
108
+ datadogLogs.addLoggerGlobalContext(key, value);
109
+ } else {
110
+ datadogRum.removeRumGlobalContext(key);
111
+ datadogLogs.removeLoggerGlobalContext(key);
112
+ }
113
+ }
114
+ },
115
+ setUser: function (user: ReportUser | null): void {
116
+ if (user) {
117
+ datadogRum.setUser({
118
+ id: user.id,
119
+ email: user.email,
120
+ name: user.name ?? user.email,
121
+ });
122
+
123
+ datadogLogs.addLoggerGlobalContext('user_id', user.id);
124
+ datadogLogs.addLoggerGlobalContext('user_email', user.email);
125
+ datadogLogs.addLoggerGlobalContext('user_username', user.username);
126
+ } else {
127
+ datadogRum.removeUser();
128
+
129
+ datadogLogs.removeLoggerGlobalContext('user_id');
130
+ datadogLogs.removeLoggerGlobalContext('user_email');
131
+ datadogLogs.removeLoggerGlobalContext('user_username');
132
+ }
133
+ },
134
+ setRouteName: function (routeName: string): void {
135
+ reporter.addMetadata({ routeName });
136
+ },
137
+ setPageName: function (pageName: string): void {
138
+ if (config.trackViewsManually) {
139
+ datadogRum.startView(pageName);
140
+ } else {
141
+ reporter.addMetadata({ pageName });
142
+ }
143
+ },
144
+ reportError: function (error: ReportError, metadata?: Metadata): void {
145
+ // Note, datadog should pick up the console error above
146
+ datadogRum.addError(error, metadata);
147
+ },
148
+ recordSession: function (): void {
149
+ datadogRum.startSessionReplayRecording();
150
+ },
151
+ recordSessionStop: function (): void {
152
+ datadogRum.stopSessionReplayRecording();
153
+ },
154
+ };
155
+ return reporter;
156
+ }
157
+
158
+ export function datadogLogger(name?: string, options?: { metadata?: Metadata }): ILogger {
159
+ const loggerName = name ?? 'root';
160
+ const ddLogger = datadogLogs.createLogger(loggerName, {
161
+ context: options?.metadata,
162
+ });
163
+ // Send to datadog and console.
164
+ ddLogger.setHandler([HandlerType.http, HandlerType.console]);
165
+
166
+ const logger: ILogger = {
167
+ startTimer: function (): Profiler {
168
+ const start = new Date();
169
+
170
+ return {
171
+ logger: this,
172
+ done: ({ message, level } = {}) => {
173
+ const duration = new Date().getTime() - start.getTime();
174
+ logger[level ?? LogLevel.Info](message ?? 'Timer Completed', { duration });
175
+ return true;
176
+ },
177
+ };
178
+ },
179
+ child: function (metadata?: Metadata, name?: string): ILogger {
180
+ return datadogLogger(name ?? `${loggerName}.child`, metadata);
181
+ },
182
+ log: (level: LogLevel, message: string, metadata?: any) => {
183
+ switch (level) {
184
+ case LogLevel.Debug:
185
+ ddLogger.debug(message, metadata);
186
+ break;
187
+
188
+ case LogLevel.Info:
189
+ ddLogger.info(message, metadata);
190
+ break;
191
+
192
+ case LogLevel.Warn:
193
+ ddLogger.warn(message, metadata);
194
+ break;
195
+
196
+ case LogLevel.Error:
197
+ ddLogger.error(message, metadata);
198
+ break;
199
+ }
200
+ return logger;
201
+ },
202
+ debug: (message: string, metadata?: any) => logger.log(LogLevel.Debug, message, metadata),
203
+ info: (message: string, metadata?: any) => logger.log(LogLevel.Info, message, metadata),
204
+ warn: (message: string, metadata?: any) => logger.log(LogLevel.Warn, message, metadata),
205
+ error: (message: string, metadata?: any) => logger.log(LogLevel.Error, message, metadata),
206
+ };
207
+ return logger;
208
+ }
@@ -0,0 +1,47 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { IReporter, ReporterBreadcrumb, ReporterEvent } from '.';
3
+ import { Metadata, ReportUser } from '../types';
4
+
5
+ declare global {
6
+ interface Window {
7
+ dataLayer?: Record<string, any>[];
8
+ }
9
+ }
10
+
11
+ export function gtmReporter(): IReporter {
12
+ const reporter: IReporter = {
13
+ addMetadata: function (metadata: Metadata): void {
14
+ window.dataLayer?.push(metadata);
15
+ },
16
+ trackEvent: function (event: ReporterEvent): void {
17
+ reporter.addMetadata({
18
+ ...event.metadata,
19
+ ...event.tags,
20
+ ...event.metrics,
21
+ event: event.message,
22
+ level: event.level,
23
+ });
24
+ },
25
+ addBreadcrumb: function (breadcrumb: ReporterBreadcrumb): void {
26
+ reporter.addMetadata({
27
+ ...breadcrumb.metadata,
28
+ event: breadcrumb.message,
29
+ });
30
+ },
31
+ setUser: function (user: ReportUser | null): void {
32
+ if (user) {
33
+ reporter.addMetadata({ user });
34
+ }
35
+ },
36
+ setRouteName: function (routeName: string): void {
37
+ reporter.addMetadata({ routeName });
38
+ },
39
+ setPageName: function (pageName: string): void {
40
+ reporter.addMetadata({ pageName });
41
+ },
42
+ reportError: function (): void {},
43
+ recordSession: function (): void {},
44
+ recordSessionStop: function (): void {},
45
+ };
46
+ return reporter;
47
+ }
@@ -0,0 +1,32 @@
1
+ import { LogLevel } from '..';
2
+ import { Metadata, Metrics, ReportError, ReportUser } from '../types';
3
+
4
+ export interface ReporterEvent {
5
+ level?: LogLevel;
6
+ message: string;
7
+ metadata?: Metadata | undefined;
8
+ tags?: Metadata | undefined;
9
+ metrics?: Metrics | undefined;
10
+ }
11
+
12
+ export interface TrackedReporterEvent extends ReporterEvent {
13
+ occurred: Date;
14
+ }
15
+
16
+ export interface ReporterBreadcrumb {
17
+ message: string;
18
+ category?: string | undefined;
19
+ metadata?: Metadata | undefined;
20
+ }
21
+
22
+ export interface IReporter {
23
+ trackEvent(event: ReporterEvent): void;
24
+ addBreadcrumb(breadcrumb: ReporterBreadcrumb): void;
25
+ addMetadata(metadata: Metadata): void;
26
+ setUser(user: ReportUser | null): void;
27
+ setRouteName(routeName: string): void;
28
+ setPageName(pageName: string): void;
29
+ reportError(error: ReportError, metadata?: Metadata): void;
30
+ recordSession(): void;
31
+ recordSessionStop(): void;
32
+ }
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ export type Metadata = Record<string, any>;
3
+
4
+ export type Metrics = Record<string, number>;
5
+
6
+ export type ServiceInfo = {
7
+ service: string;
8
+ environment: string;
9
+ version: string;
10
+ defaultMetadata?: Metadata;
11
+ };
12
+
13
+ export type ReportUser = { id: string; email?: string; username?: string; name?: string };
14
+
15
+ export type ReportError = Error | string;
16
+
17
+ export enum LogLevel {
18
+ Debug = 'debug',
19
+ Info = 'info',
20
+ Warn = 'warn',
21
+ Error = 'error',
22
+ }
23
+
24
+ export interface LogLevelMethod {
25
+ (message: string, metadata?: any): ILogger;
26
+ (message: string, ...meta: any[]): ILogger;
27
+ (message: any): ILogger;
28
+ }
29
+
30
+ export interface LogMethod {
31
+ (level: LogLevel, message: string, metadata?: any): ILogger;
32
+ (level: LogLevel, message: string, ...meta: any[]): ILogger;
33
+ (level: LogLevel, message: any): ILogger;
34
+ }
35
+
36
+ export interface Profiler {
37
+ logger: ILogger;
38
+ done(info?: { message?: string; level?: LogLevel }): boolean;
39
+ }
40
+
41
+ export interface ILogger {
42
+ startTimer(): Profiler;
43
+ child(metadata?: Metadata, name?: string): ILogger;
44
+
45
+ log: LogMethod;
46
+ debug: LogLevelMethod;
47
+ info: LogLevelMethod;
48
+ warn: LogLevelMethod;
49
+ error: LogLevelMethod;
50
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,50 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { ILogger, Profiler, LogMethod, LogLevelMethod, Metadata, LogLevel } from '.';
4
+
5
+ export function consoleLogger(name?: string, options?: { metadata?: Metadata }): ILogger {
6
+ const logger: ILogger = {
7
+ startTimer(): Profiler {
8
+ const start = new Date();
9
+
10
+ return {
11
+ logger: this,
12
+ done: (args) => {
13
+ const duration = new Date().getTime() - start.getTime();
14
+ logger[args?.level ?? LogLevel.Info](`${args?.message ?? 'Timer'} completed in: ${duration}`);
15
+ return true;
16
+ },
17
+ };
18
+ },
19
+
20
+ child(metadata?: Record<string, any>, name?: string): ILogger {
21
+ return consoleLogger(name, { metadata });
22
+ },
23
+
24
+ log: (level: LogLevel, message: string, metadata?: any) => {
25
+ return logger[level](message, message);
26
+ },
27
+
28
+ debug: (...args: any[]) => {
29
+ console.debug(...args);
30
+ return logger;
31
+ },
32
+
33
+ info: (...args: any[]) => {
34
+ console.log(...args);
35
+ return logger;
36
+ },
37
+
38
+ warn: (...args: any[]) => {
39
+ console.warn(...args);
40
+ return logger;
41
+ },
42
+
43
+ error: (...args: any[]) => {
44
+ console.error(...args);
45
+ return logger;
46
+ },
47
+ };
48
+
49
+ return logger;
50
+ }