dce-reactkit 3.1.20 → 3.2.0-beta.10

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 (59) hide show
  1. package/.vscode/settings.json +6 -0
  2. package/dist/cjs/index.js +517 -21
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/constants/LOG_ROUTE_PATH.d.ts +6 -0
  5. package/dist/cjs/types/constants/ROUTE_PATH_PREFIX.d.ts +6 -0
  6. package/dist/cjs/types/helpers/genRouteHandler.d.ts +2 -0
  7. package/dist/cjs/types/helpers/logClientEvent.d.ts +7 -0
  8. package/dist/cjs/types/helpers/parseUserAgent.d.ts +17 -0
  9. package/dist/cjs/types/index.d.ts +8 -1
  10. package/dist/cjs/types/server/initLogCollection.d.ts +8 -0
  11. package/dist/cjs/types/server/initServer.d.ts +13 -0
  12. package/dist/cjs/types/types/Log/LogMainInfo.d.ts +37 -0
  13. package/dist/cjs/types/types/Log/LogSourceSpecificInfo.d.ts +13 -0
  14. package/dist/cjs/types/types/Log/LogTypeSpecificInfo.d.ts +17 -0
  15. package/dist/cjs/types/types/Log/index.d.ts +10 -0
  16. package/dist/cjs/types/types/LogAction.d.ts +23 -0
  17. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +15 -0
  18. package/dist/cjs/types/types/LogFunction.d.ts +24 -0
  19. package/dist/cjs/types/types/LogSource.d.ts +9 -0
  20. package/dist/cjs/types/types/LogType.d.ts +9 -0
  21. package/dist/esm/index.js +512 -22
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/types/constants/LOG_ROUTE_PATH.d.ts +6 -0
  24. package/dist/esm/types/constants/ROUTE_PATH_PREFIX.d.ts +6 -0
  25. package/dist/esm/types/helpers/genRouteHandler.d.ts +2 -0
  26. package/dist/esm/types/helpers/logClientEvent.d.ts +7 -0
  27. package/dist/esm/types/helpers/parseUserAgent.d.ts +17 -0
  28. package/dist/esm/types/index.d.ts +8 -1
  29. package/dist/esm/types/server/initLogCollection.d.ts +8 -0
  30. package/dist/esm/types/server/initServer.d.ts +13 -0
  31. package/dist/esm/types/types/Log/LogMainInfo.d.ts +37 -0
  32. package/dist/esm/types/types/Log/LogSourceSpecificInfo.d.ts +13 -0
  33. package/dist/esm/types/types/Log/LogTypeSpecificInfo.d.ts +17 -0
  34. package/dist/esm/types/types/Log/index.d.ts +10 -0
  35. package/dist/esm/types/types/LogAction.d.ts +23 -0
  36. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +15 -0
  37. package/dist/esm/types/types/LogFunction.d.ts +24 -0
  38. package/dist/esm/types/types/LogSource.d.ts +9 -0
  39. package/dist/esm/types/types/LogType.d.ts +9 -0
  40. package/dist/index.d.ts +171 -1
  41. package/package.json +1 -1
  42. package/src/components/AppWrapper.tsx +1 -1
  43. package/src/constants/LOG_ROUTE_PATH.ts +9 -0
  44. package/src/constants/ROUTE_PATH_PREFIX.ts +7 -0
  45. package/src/helpers/genRouteHandler.ts +233 -2
  46. package/src/helpers/logClientEvent.tsx +68 -0
  47. package/src/helpers/parseUserAgent.ts +108 -0
  48. package/src/index.ts +14 -0
  49. package/src/server/initLogCollection.ts +22 -0
  50. package/src/server/initServer.ts +90 -0
  51. package/src/types/Log/LogMainInfo.ts +64 -0
  52. package/src/types/Log/LogSourceSpecificInfo.ts +25 -0
  53. package/src/types/Log/LogTypeSpecificInfo.ts +33 -0
  54. package/src/types/Log/index.ts +17 -0
  55. package/src/types/LogAction.ts +40 -0
  56. package/src/types/LogBuiltInMetadata.ts +18 -0
  57. package/src/types/LogFunction.ts +43 -0
  58. package/src/types/LogSource.ts +12 -0
  59. package/src/types/LogType.ts +12 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Built-in metadata for logs
3
+ * @author Gabe Abrams
4
+ */
5
+ declare const LogBuiltInMetadata: {
6
+ Context: {
7
+ Uncategorized: string;
8
+ ServerRenderedErrorPage: string;
9
+ ServerEndpointError: string;
10
+ };
11
+ Target: {
12
+ NoSpecificTarget: string;
13
+ };
14
+ };
15
+ export default LogBuiltInMetadata;
@@ -0,0 +1,24 @@
1
+ import Log from './Log';
2
+ import LogAction from './LogAction';
3
+ /**
4
+ * Type of a log action function
5
+ * @author Gabe Abrams
6
+ */
7
+ declare type LogFunction = (opts: ({
8
+ context: string | {
9
+ _: string;
10
+ };
11
+ subcontext?: string | {
12
+ _: string;
13
+ };
14
+ tags?: string[];
15
+ metadata?: {
16
+ [k: string]: any;
17
+ };
18
+ } & ({
19
+ error: any;
20
+ } | {
21
+ action: LogAction;
22
+ target?: string;
23
+ }))) => Promise<Log>;
24
+ export default LogFunction;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Source of a log event
3
+ * @author Gabe Abrams
4
+ */
5
+ declare enum LogSource {
6
+ Client = "client",
7
+ Server = "server"
8
+ }
9
+ export default LogSource;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Type of a log event
3
+ * @author Gabe Abrams
4
+ */
5
+ declare enum LogType {
6
+ Action = "action",
7
+ Error = "error"
8
+ }
9
+ export default LogType;
package/dist/index.d.ts CHANGED
@@ -515,6 +515,140 @@ declare enum ParamType {
515
515
  StringOptional = "string-optional"
516
516
  }
517
517
 
518
+ /**
519
+ * Main information in a log event
520
+ * @author Gabe Abrams
521
+ */
522
+ declare type LogMainInfo = {
523
+ id: string;
524
+ userFirstName: string;
525
+ userLastName: string;
526
+ userEmail: string;
527
+ userId: number;
528
+ isLearner: boolean;
529
+ isAdmin: boolean;
530
+ isTTM: boolean;
531
+ courseId: number;
532
+ courseName: string;
533
+ browser: {
534
+ name: string;
535
+ version: string;
536
+ };
537
+ device: {
538
+ os: string;
539
+ isMobile: boolean;
540
+ };
541
+ year: number;
542
+ month: number;
543
+ day: number;
544
+ hour: number;
545
+ minute: number;
546
+ timestamp: number;
547
+ context: string;
548
+ subcontext: string;
549
+ tags: string[];
550
+ metadata?: {
551
+ [k: string]: any;
552
+ };
553
+ };
554
+
555
+ /**
556
+ * Source of a log event
557
+ * @author Gabe Abrams
558
+ */
559
+ declare enum LogSource {
560
+ Client = "client",
561
+ Server = "server"
562
+ }
563
+
564
+ /**
565
+ * Log info that is specific to the type of source
566
+ * @author Gabe Abrams
567
+ */
568
+ declare type LogSourceSpecificInfo = ({
569
+ source: LogSource.Client;
570
+ } | {
571
+ source: LogSource.Server;
572
+ routePath: string;
573
+ routeTemplate: string;
574
+ });
575
+
576
+ /**
577
+ * Types of actions
578
+ * @author Gabe Abrams
579
+ */
580
+ declare enum LogAction {
581
+ Open = "open",
582
+ Close = "close",
583
+ Cancel = "cancel",
584
+ Expand = "expand",
585
+ Collapse = "collapse",
586
+ View = "view",
587
+ Interrupt = "interrupt",
588
+ Create = "create",
589
+ Edit = "edit",
590
+ Delete = "delete",
591
+ Add = "add",
592
+ Remove = "remove",
593
+ Activate = "activate",
594
+ Deactivate = "deactivate",
595
+ Peek = "peek",
596
+ Unknown = "unknown"
597
+ }
598
+
599
+ /**
600
+ * Type of a log event
601
+ * @author Gabe Abrams
602
+ */
603
+ declare enum LogType {
604
+ Action = "action",
605
+ Error = "error"
606
+ }
607
+
608
+ /**
609
+ * Log info that is specific to the type of log
610
+ * @author Gabe Abrams
611
+ */
612
+ declare type LogTypeSpecificInfo = ({
613
+ type: LogType.Error;
614
+ errorMessage: string;
615
+ errorCode: string;
616
+ errorStack: string;
617
+ } | {
618
+ type: LogType.Action;
619
+ target: string;
620
+ action: LogAction;
621
+ });
622
+
623
+ /**
624
+ * A single log event corresponding to an action performed by a user or an
625
+ * error encountered by a user
626
+ * @author Gabe Abrams
627
+ */
628
+ declare type Log = (LogMainInfo & LogSourceSpecificInfo & LogTypeSpecificInfo);
629
+
630
+ /**
631
+ * Type of a log action function
632
+ * @author Gabe Abrams
633
+ */
634
+ declare type LogFunction = (opts: ({
635
+ context: string | {
636
+ _: string;
637
+ };
638
+ subcontext?: string | {
639
+ _: string;
640
+ };
641
+ tags?: string[];
642
+ metadata?: {
643
+ [k: string]: any;
644
+ };
645
+ } & ({
646
+ error: any;
647
+ } | {
648
+ action: LogAction;
649
+ target?: string;
650
+ }))) => Promise<Log>;
651
+
518
652
  /**
519
653
  * Generate an express API route handler
520
654
  * @author Gabe Abrams
@@ -556,6 +690,7 @@ declare const genRouteHandler: (opts: {
556
690
  pageTitle?: string | undefined;
557
691
  status?: number | undefined;
558
692
  } | undefined) => void;
693
+ logServerEvent: LogFunction;
559
694
  }) => any;
560
695
  skipSessionCheck?: boolean | undefined;
561
696
  }) => (req: any, res: any, next: () => void) => Promise<undefined>;
@@ -594,10 +729,16 @@ declare type GetLaunchInfoFunction = (req: any) => {
594
729
  * Prepare dce-reactkit to run on the server
595
730
  * @author Gabe Abrams
596
731
  * @param opts object containing all arguments
732
+ * @param opts.app express app from inside of the postprocessor function that
733
+ * we will add routes to
597
734
  * @param opts.getLaunchInfo CACCL LTI's get launch info function
735
+ * @param [opts.logCollection] mongo collection from dce-mango to use for
736
+ * storing logs. If none is included, logs are written to the console
598
737
  */
599
738
  declare const initServer: (opts: {
739
+ app: any;
600
740
  getLaunchInfo: GetLaunchInfoFunction;
741
+ logCollection?: any;
601
742
  }) => void;
602
743
 
603
744
  /**
@@ -696,6 +837,20 @@ declare const onlyKeepLetters: (str: string) => string;
696
837
  */
697
838
  declare const parallelLimit: (taskFunctions: (() => Promise<any>)[], limit?: number | undefined) => Promise<any[]>;
698
839
 
840
+ /**
841
+ * Log a user action on the client (cannot be used on the server)
842
+ * @author Gabe Abrams
843
+ */
844
+ declare const logClientEvent: LogFunction;
845
+
846
+ /**
847
+ * Initialize a log collection given the dce-mango Collection class
848
+ * @author Gabe Abrams
849
+ * @param Collection the Collection class from dce-mango
850
+ * @returns initialized logCollection
851
+ */
852
+ declare const initLogCollection: (Collection: any) => any;
853
+
699
854
  /**
700
855
  * List of error codes built into the react kit
701
856
  * @author Gabe Abrams
@@ -727,4 +882,19 @@ declare enum DayOfWeek {
727
882
  Sunday = "u"
728
883
  }
729
884
 
730
- export { AppWrapper, ButtonInputGroup, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, ItemPicker, LoadingSpinner, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initServer, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
885
+ /**
886
+ * Built-in metadata for logs
887
+ * @author Gabe Abrams
888
+ */
889
+ declare const LogBuiltInMetadata: {
890
+ Context: {
891
+ Uncategorized: string;
892
+ ServerRenderedErrorPage: string;
893
+ ServerEndpointError: string;
894
+ };
895
+ Target: {
896
+ NoSpecificTarget: string;
897
+ };
898
+ };
899
+
900
+ export { AppWrapper, ButtonInputGroup, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, ItemPicker, LoadingSpinner, Log, LogAction, LogBuiltInMetadata, LogSource, LogType, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initLogCollection, initServer, logClientEvent, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.1.20",
3
+ "version": "3.2.0-beta.10",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -303,7 +303,7 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
303
303
  undefined
304
304
  | {
305
305
  title: string,
306
- text: string
306
+ text: string,
307
307
  }
308
308
  >(undefined);
309
309
  setAlertInfo = setAlertInfoInner;
@@ -0,0 +1,9 @@
1
+ import ROUTE_PATH_PREFIX from './ROUTE_PATH_PREFIX';
2
+
3
+ /**
4
+ * Path of the route for storing client-side logs
5
+ * @author Gabe Abrams
6
+ */
7
+ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
8
+
9
+ export default LOG_ROUTE_PATH;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Path that all routes start with
3
+ * @author Gabe Abrams
4
+ */
5
+ const ROUTE_PATH_PREFIX = '/dce-reactkit';
6
+
7
+ export default ROUTE_PATH_PREFIX;
@@ -1,5 +1,8 @@
1
1
  // Import caccl functions
2
- import { cacclGetLaunchInfo } from '../server/initServer';
2
+ import {
3
+ cacclGetLaunchInfo,
4
+ internalGetLogCollection,
5
+ } from '../server/initServer';
3
6
 
4
7
  // Import shared types
5
8
  import ReactKitErrorCode from '../types/ReactKitErrorCode';
@@ -9,6 +12,19 @@ import ParamType from '../types/ParamType';
9
12
  import handleError from './handleError';
10
13
  import handleSuccess from './handleSuccess';
11
14
  import genErrorPage from '../html/genErrorPage';
15
+ import parseUserAgent from './parseUserAgent';
16
+ import getTimeInfoInET from './getTimeInfoInET';
17
+
18
+ // Import shared types
19
+ import LogFunction from '../types/LogFunction';
20
+ import Log from '../types/Log';
21
+ import LogType from '../types/LogType';
22
+ import LogSource from '../types/LogSource';
23
+ import LogTypeSpecificInfo from '../types/Log/LogTypeSpecificInfo';
24
+ import LogMainInfo from '../types/Log/LogMainInfo';
25
+ import LogSourceSpecificInfo from '../types/Log/LogSourceSpecificInfo';
26
+ import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
27
+ import LogAction from '../types/LogAction';
12
28
 
13
29
  /**
14
30
  * Generate an express API route handler
@@ -55,6 +71,7 @@ const genRouteHandler = (
55
71
  status?: number,
56
72
  },
57
73
  ) => void,
74
+ logServerEvent: LogFunction,
58
75
  },
59
76
  ) => any,
60
77
  skipSessionCheck?: boolean,
@@ -439,6 +456,195 @@ const genRouteHandler = (
439
456
  );
440
457
  }
441
458
 
459
+ /*----------------------------------------*/
460
+ /* Log Handler */
461
+ /*----------------------------------------*/
462
+
463
+ // Create a log handler function
464
+
465
+ /**
466
+ * Log an event on the server
467
+ * @author Gabe Abrams
468
+ */
469
+ const logServerEvent: LogFunction = async (opts) => {
470
+ // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
471
+ // that indicates that this is actually a client event, but we don't
472
+ // include that in the LogFunction type because this is internal and
473
+ // hidden from users
474
+ try {
475
+ // Parse user agent
476
+ const {
477
+ browser,
478
+ device,
479
+ } = parseUserAgent(req.headers['user-agent']);
480
+
481
+ // Get time info in ET
482
+ const {
483
+ timestamp,
484
+ year,
485
+ month,
486
+ day,
487
+ hour,
488
+ minute,
489
+ } = getTimeInfoInET();
490
+
491
+ // Main log info
492
+ const mainLogInfo: LogMainInfo = {
493
+ id: `${launchInfo.userId}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
494
+ userFirstName: launchInfo.userFirstName,
495
+ userLastName: launchInfo.userLastName,
496
+ userEmail: launchInfo.userEmail,
497
+ userId: launchInfo.userId,
498
+ isLearner: !!launchInfo.isLearner,
499
+ isAdmin: !!launchInfo.isAdmin,
500
+ isTTM: !!launchInfo.isTTM,
501
+ courseId: launchInfo.courseId,
502
+ courseName: launchInfo.courseName,
503
+ browser,
504
+ device,
505
+ year,
506
+ month,
507
+ day,
508
+ hour,
509
+ minute,
510
+ timestamp,
511
+ context: (
512
+ typeof opts.context === 'string'
513
+ ? opts.context
514
+ : (
515
+ ((opts.context as any) ?? {})._
516
+ ?? LogBuiltInMetadata.Context.Uncategorized
517
+ )
518
+ ),
519
+ subcontext: (
520
+ typeof opts.context === 'string'
521
+ ? opts.subcontext
522
+ : (
523
+ ((opts.subcontext as any) ?? {})._
524
+ ?? LogBuiltInMetadata.Context.Uncategorized
525
+ )
526
+ ),
527
+ tags: opts.tags ?? [],
528
+ metadata: opts.metadata ?? {},
529
+ };
530
+
531
+ // Type-specific info
532
+ const typeSpecificInfo: LogTypeSpecificInfo = (
533
+ ('error' in opts && opts.error)
534
+ ? {
535
+ type: LogType.Error,
536
+ errorMessage: opts.error.message ?? 'Unknown message',
537
+ errorCode: opts.error.code ?? ReactKitErrorCode.NoCode,
538
+ errorStack: opts.error.stack ?? 'No stack',
539
+ }
540
+ : {
541
+ type: LogType.Action,
542
+ target: (
543
+ (opts as any).target
544
+ ?? LogBuiltInMetadata.Target.NoSpecificTarget
545
+ ),
546
+ action: (
547
+ (opts as any).action
548
+ ?? LogAction.Unknown
549
+ ),
550
+ }
551
+ );
552
+
553
+ // Source-specific info
554
+ const sourceSpecificInfo: LogSourceSpecificInfo = (
555
+ (opts as any).overrideAsClientEvent
556
+ ? {
557
+ source: LogSource.Client,
558
+ }
559
+ : {
560
+ source: LogSource.Server,
561
+ routePath: req.path,
562
+ routeTemplate: req.route.path,
563
+ }
564
+ );
565
+
566
+ // Build log event
567
+ const log: Log = {
568
+ ...mainLogInfo,
569
+ ...typeSpecificInfo,
570
+ ...sourceSpecificInfo,
571
+ };
572
+
573
+ // Either print to console or save to db
574
+ const logCollection = internalGetLogCollection();
575
+ if (logCollection) {
576
+ // Store to the log collection
577
+ await logCollection.insert(log);
578
+ } else {
579
+ // Print to console
580
+ if (log.type === LogType.Error) {
581
+ console.error('dce-reactkit error log:', log);
582
+ } else {
583
+ console.log('dce-reactkit action log:', log);
584
+ }
585
+ }
586
+
587
+ // Return log entry
588
+ return log;
589
+ } catch (err) {
590
+ // Print because we cannot store the error
591
+ console.error('Could not log the following:', opts);
592
+
593
+ // Create a dummy log to return
594
+ const dummyMainInfo: LogMainInfo = {
595
+ id: '-1',
596
+ userFirstName: 'Unknown',
597
+ userLastName: 'Unknown',
598
+ userEmail: 'unknown@harvard.edu',
599
+ userId: 1,
600
+ isLearner: false,
601
+ isAdmin: false,
602
+ isTTM: false,
603
+ courseId: 1,
604
+ courseName: 'Unknown',
605
+ browser: {
606
+ name: 'Unknown',
607
+ version: 'Unknown',
608
+ },
609
+ device: {
610
+ isMobile: false,
611
+ os: 'Unknown',
612
+ },
613
+ year: 1,
614
+ month: 1,
615
+ day: 1,
616
+ hour: 1,
617
+ minute: 1,
618
+ timestamp: Date.now(),
619
+ tags: [],
620
+ metadata: {},
621
+ context: LogBuiltInMetadata.Context.Uncategorized,
622
+ subcontext: LogBuiltInMetadata.Context.Uncategorized,
623
+ };
624
+
625
+ const dummyTypeSpecificInfo: LogTypeSpecificInfo = {
626
+ type: LogType.Error,
627
+ errorMessage: 'Unknown',
628
+ errorCode: 'Unknown',
629
+ errorStack: 'No Stack',
630
+ };
631
+
632
+ const dummySourceSpecificInfo: LogSourceSpecificInfo = {
633
+ source: LogSource.Server,
634
+ routePath: req.path,
635
+ routeTemplate: req.route.path,
636
+ };
637
+
638
+ const log: Log = {
639
+ ...dummyMainInfo,
640
+ ...dummyTypeSpecificInfo,
641
+ ...dummySourceSpecificInfo,
642
+ };
643
+
644
+ return log;
645
+ }
646
+ };
647
+
442
648
  /*------------------------------------------------------------------------*/
443
649
  /* Call handler */
444
650
  /*------------------------------------------------------------------------*/
@@ -490,6 +696,22 @@ const genRouteHandler = (
490
696
  ) => {
491
697
  const html = genErrorPage(opts);
492
698
  send(html, opts.status ?? 500);
699
+
700
+ // Log
701
+ logServerEvent({
702
+ context: LogBuiltInMetadata.Context.ServerRenderedErrorPage,
703
+ error: {
704
+ message: `${opts.title}: ${opts.description}`,
705
+ code: opts.code,
706
+ },
707
+ metadata: {
708
+ title: opts.title,
709
+ description: opts.description,
710
+ code: opts.code,
711
+ pageTitle: opts.pageTitle,
712
+ status: opts.status ?? 500,
713
+ },
714
+ });
493
715
  };
494
716
 
495
717
  // Call the handler
@@ -504,6 +726,7 @@ const genRouteHandler = (
504
726
  },
505
727
  redirect,
506
728
  renderErrorPage,
729
+ logServerEvent,
507
730
  });
508
731
 
509
732
  // Send results to client (only if next wasn't called)
@@ -513,7 +736,15 @@ const genRouteHandler = (
513
736
  } catch (err) {
514
737
  // Send error to client (only if next wasn't called)
515
738
  if (!responseSent) {
516
- return handleError(res, err);
739
+ handleError(res, err);
740
+
741
+ // Log server-side error
742
+ logServerEvent({
743
+ context: LogBuiltInMetadata.Context.ServerEndpointError,
744
+ error: err,
745
+ });
746
+
747
+ return;
517
748
  }
518
749
 
519
750
  // Log error that was not responded with
@@ -0,0 +1,68 @@
1
+ // Import shared types
2
+ import LOG_ROUTE_PATH from '../constants/LOG_ROUTE_PATH';
3
+ import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
4
+ import LogFunction from '../types/LogFunction';
5
+
6
+ // Import shared functions
7
+ import visitServerEndpoint from './visitServerEndpoint';
8
+
9
+ /**
10
+ * Log a user action on the client (cannot be used on the server)
11
+ * @author Gabe Abrams
12
+ */
13
+ const logClientEvent: LogFunction = async (opts) => {
14
+ return visitServerEndpoint({
15
+ path: LOG_ROUTE_PATH,
16
+ method: 'POST',
17
+ params: {
18
+ context: (
19
+ typeof opts.context === 'string'
20
+ ? opts.context
21
+ : (
22
+ ((opts.context as any) ?? {})._
23
+ ?? LogBuiltInMetadata.Context.Uncategorized
24
+ )
25
+ ),
26
+ subcontext: (
27
+ typeof opts.context === 'string'
28
+ ? opts.subcontext
29
+ : (
30
+ ((opts.subcontext as any) ?? {})._
31
+ ?? LogBuiltInMetadata.Context.Uncategorized
32
+ )
33
+ ),
34
+ tags: JSON.stringify(opts.tags ?? []),
35
+ metadata: JSON.stringify(opts.metadata ?? {}),
36
+ errorMessage: (
37
+ (opts as any).error
38
+ ? (opts as any).error.message
39
+ : undefined
40
+ ),
41
+ errorCode: (
42
+ (opts as any).error
43
+ ? (opts as any).error.code
44
+ : undefined
45
+ ),
46
+ errorStack: (
47
+ (opts as any).error
48
+ ? (opts as any).error.stack
49
+ : undefined
50
+ ),
51
+ target: (
52
+ (opts as any).action
53
+ ? (
54
+ (opts as any).target
55
+ ?? LogBuiltInMetadata.Target.NoSpecificTarget
56
+ )
57
+ : undefined
58
+ ),
59
+ action: (
60
+ (opts as any).action
61
+ ? (opts as any).action
62
+ : undefined
63
+ ),
64
+ },
65
+ });
66
+ };
67
+
68
+ export default logClientEvent;