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,6 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Subcontext",
4
+ "Uncategorized"
5
+ ]
6
+ }
package/dist/cjs/index.js CHANGED
@@ -2124,9 +2124,42 @@ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function
2124
2124
  return body;
2125
2125
  });
2126
2126
 
2127
+ /**
2128
+ * Path that all routes start with
2129
+ * @author Gabe Abrams
2130
+ */
2131
+ const ROUTE_PATH_PREFIX = '/dce-reactkit';
2132
+
2133
+ /**
2134
+ * Path of the route for storing client-side logs
2135
+ * @author Gabe Abrams
2136
+ */
2137
+ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
2138
+
2139
+ /**
2140
+ * Server-side API param types
2141
+ * @author Gabe Abrams
2142
+ */
2143
+ var ParamType;
2144
+ (function (ParamType) {
2145
+ ParamType["Boolean"] = "boolean";
2146
+ ParamType["BooleanOptional"] = "boolean-optional";
2147
+ ParamType["Float"] = "float";
2148
+ ParamType["FloatOptional"] = "float-optional";
2149
+ ParamType["Int"] = "int";
2150
+ ParamType["IntOptional"] = "int-optional";
2151
+ ParamType["JSON"] = "json";
2152
+ ParamType["JSONOptional"] = "json-optional";
2153
+ ParamType["String"] = "string";
2154
+ ParamType["StringOptional"] = "string-optional";
2155
+ })(ParamType || (ParamType = {}));
2156
+ var ParamType$1 = ParamType;
2157
+
2127
2158
  // Import custom error
2128
2159
  // Stored copy of caccl functions
2129
2160
  let _cacclGetLaunchInfo;
2161
+ // Stored copy of dce-mango log collection
2162
+ let _logCollection;
2130
2163
  /*------------------------------------------------------------------------*/
2131
2164
  /* Helpers */
2132
2165
  /*------------------------------------------------------------------------*/
@@ -2142,6 +2175,15 @@ const cacclGetLaunchInfo = (req) => {
2142
2175
  }
2143
2176
  return _cacclGetLaunchInfo(req);
2144
2177
  };
2178
+ /**
2179
+ * Get log collection
2180
+ * @author Gabe Abrams
2181
+ * @returns log collection if one was included during launch or null if we don't
2182
+ * have a log collection (yet)
2183
+ */
2184
+ const internalGetLogCollection = () => {
2185
+ return _logCollection !== null && _logCollection !== void 0 ? _logCollection : null;
2186
+ };
2145
2187
  /*------------------------------------------------------------------------*/
2146
2188
  /* Main */
2147
2189
  /*------------------------------------------------------------------------*/
@@ -2149,31 +2191,73 @@ const cacclGetLaunchInfo = (req) => {
2149
2191
  * Prepare dce-reactkit to run on the server
2150
2192
  * @author Gabe Abrams
2151
2193
  * @param opts object containing all arguments
2194
+ * @param opts.app express app from inside of the postprocessor function that
2195
+ * we will add routes to
2152
2196
  * @param opts.getLaunchInfo CACCL LTI's get launch info function
2197
+ * @param [opts.logCollection] mongo collection from dce-mango to use for
2198
+ * storing logs. If none is included, logs are written to the console
2153
2199
  */
2154
2200
  const initServer = (opts) => {
2155
2201
  _cacclGetLaunchInfo = opts.getLaunchInfo;
2202
+ _logCollection = opts.logCollection;
2203
+ /**
2204
+ * Log an event
2205
+ * @author Gabe Abrams
2206
+ * @param {string} context Context of the event (each app determines how to
2207
+ * organize its contexts)
2208
+ * @param {string} subcontext Subcontext of the event (each app determines
2209
+ * how to organize its subcontexts)
2210
+ * @param {string} tags stringified list of tags that apply to this action
2211
+ * (each app determines tag usage)
2212
+ * @param {string} metadata stringified object containing optional custom metadata
2213
+ * @param {string} [errorMessage] error message if type is an error
2214
+ * @param {string} [errorCode] error code if type is an error
2215
+ * @param {string} [errorStack] error stack if type is an error
2216
+ * @param {string} [target] Target of the action (each app determines the list
2217
+ * of targets) These are usually buttons, panels, elements, etc.
2218
+ * @param {LogAction} [action] the type of action performed on the target
2219
+ * @returns {Log}
2220
+ */
2221
+ opts.app.post(LOG_ROUTE_PATH, genRouteHandler({
2222
+ paramTypes: {
2223
+ context: ParamType$1.String,
2224
+ subcontext: ParamType$1.String,
2225
+ tags: ParamType$1.JSON,
2226
+ metadata: ParamType$1.JSON,
2227
+ errorMessage: ParamType$1.StringOptional,
2228
+ errorCode: ParamType$1.StringOptional,
2229
+ errorStack: ParamType$1.StringOptional,
2230
+ target: ParamType$1.StringOptional,
2231
+ action: ParamType$1.StringOptional,
2232
+ },
2233
+ handler: ({ params, logServerEvent }) => {
2234
+ const log = logServerEvent((params.errorMessage || params.errorCode || params.errorStack)
2235
+ // Error
2236
+ ? {
2237
+ context: params.context,
2238
+ subcontext: params.subcontext,
2239
+ tags: params.tags,
2240
+ metadata: params.metadata,
2241
+ error: {
2242
+ message: params.errorMessage,
2243
+ code: params.errorCode,
2244
+ stack: params.errorStack,
2245
+ },
2246
+ }
2247
+ // Action
2248
+ : {
2249
+ context: params.context,
2250
+ subcontext: params.subcontext,
2251
+ tags: params.tags,
2252
+ metadata: params.metadata,
2253
+ target: params.target,
2254
+ action: params.action,
2255
+ });
2256
+ return log;
2257
+ },
2258
+ }));
2156
2259
  };
2157
2260
 
2158
- /**
2159
- * Server-side API param types
2160
- * @author Gabe Abrams
2161
- */
2162
- var ParamType;
2163
- (function (ParamType) {
2164
- ParamType["Boolean"] = "boolean";
2165
- ParamType["BooleanOptional"] = "boolean-optional";
2166
- ParamType["Float"] = "float";
2167
- ParamType["FloatOptional"] = "float-optional";
2168
- ParamType["Int"] = "int";
2169
- ParamType["IntOptional"] = "int-optional";
2170
- ParamType["JSON"] = "json";
2171
- ParamType["JSONOptional"] = "json-optional";
2172
- ParamType["String"] = "string";
2173
- ParamType["StringOptional"] = "string-optional";
2174
- })(ParamType || (ParamType = {}));
2175
- var ParamType$1 = ParamType;
2176
-
2177
2261
  // Import shared types
2178
2262
  /**
2179
2263
  * Handle an error and respond to the client
@@ -2366,6 +2450,195 @@ const genErrorPage = (opts = {}) => {
2366
2450
  `;
2367
2451
  };
2368
2452
 
2453
+ /**
2454
+ * Perform a rudimentary parsing of the user's browser agent string
2455
+ * @author Gabe Abrams
2456
+ * @param userAgent the user's browser agent
2457
+ * @returns user info
2458
+ */
2459
+ const parseUserAgent = (userAgent) => {
2460
+ /* ------------- Browser ------------ */
2461
+ let browser = {
2462
+ name: 'Unknown',
2463
+ version: 'Unknown',
2464
+ };
2465
+ // Parse user agent
2466
+ let verOffset;
2467
+ let nameOffset;
2468
+ if ((verOffset = userAgent.indexOf('Opera')) !== -1) {
2469
+ // In Opera, the true version is after 'Opera' or after 'Version'
2470
+ browser = {
2471
+ name: 'Opera',
2472
+ version: userAgent.substring(verOffset + 6),
2473
+ };
2474
+ if ((verOffset = userAgent.indexOf('Version')) !== -1) {
2475
+ browser.version = userAgent.substring(verOffset + 8);
2476
+ }
2477
+ }
2478
+ else if ((verOffset = userAgent.indexOf('MSIE')) !== -1) {
2479
+ // In MSIE, the true version is after 'MSIE' in userAgent
2480
+ browser = {
2481
+ name: 'Internet Explorer',
2482
+ version: userAgent.substring(verOffset + 5),
2483
+ };
2484
+ }
2485
+ else if ((verOffset = userAgent.indexOf('Chrome')) !== -1) {
2486
+ // In Chrome, the true version is after 'Chrome'
2487
+ browser = {
2488
+ name: 'Chrome',
2489
+ version: userAgent.substring(verOffset + 7),
2490
+ };
2491
+ }
2492
+ else if ((verOffset = userAgent.indexOf('Safari')) !== -1) {
2493
+ // In Safari, the true version is after 'Safari' or after 'Version'
2494
+ browser = {
2495
+ name: 'Safari',
2496
+ version: userAgent.substring(verOffset + 7),
2497
+ };
2498
+ if ((verOffset = userAgent.indexOf('Version')) !== -1) {
2499
+ browser.version = userAgent.substring(verOffset + 8);
2500
+ }
2501
+ }
2502
+ else if ((verOffset = userAgent.indexOf('Firefox')) != -1) {
2503
+ // In Firefox, the true version is after 'Firefox'
2504
+ browser = {
2505
+ name: 'Firefox',
2506
+ version: userAgent.substring(verOffset + 8),
2507
+ };
2508
+ }
2509
+ else if ((nameOffset = userAgent.lastIndexOf(' ') + 1)
2510
+ < (verOffset = userAgent.lastIndexOf('/'))) {
2511
+ browser = {
2512
+ name: userAgent.substring(nameOffset, verOffset),
2513
+ version: userAgent.substring(verOffset + 1),
2514
+ };
2515
+ }
2516
+ // Postprocess version
2517
+ // trim the fullVersion string at semicolon/space if present
2518
+ let ix;
2519
+ if ((ix = browser.version.indexOf(';')) !== -1) {
2520
+ browser.version = browser.version.substring(0, ix);
2521
+ }
2522
+ if ((ix = browser.version.indexOf(' ')) !== -1) {
2523
+ browser.version = browser.version.substring(0, ix);
2524
+ }
2525
+ /* ------------- Device ------------- */
2526
+ // Detect os
2527
+ let os = 'Unknown';
2528
+ if (userAgent.includes('Linux')) {
2529
+ os = 'Linux';
2530
+ }
2531
+ else if (userAgent.includes('like Mac')) {
2532
+ os = 'iOS';
2533
+ }
2534
+ else if (userAgent.includes('Mac')) {
2535
+ os = 'Mac';
2536
+ }
2537
+ else if (userAgent.includes('Android')) {
2538
+ os = 'Android';
2539
+ }
2540
+ else if (userAgent.includes('Win')) {
2541
+ os = 'Win';
2542
+ }
2543
+ // Check if mobile
2544
+ const isMobile = !!userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/);
2545
+ // Device
2546
+ const device = {
2547
+ isMobile,
2548
+ os,
2549
+ };
2550
+ /* ------------- Finish ------------- */
2551
+ // Return info
2552
+ return {
2553
+ browser,
2554
+ device,
2555
+ };
2556
+ };
2557
+
2558
+ /**
2559
+ * Type of a log event
2560
+ * @author Gabe Abrams
2561
+ */
2562
+ var LogType;
2563
+ (function (LogType) {
2564
+ // User action
2565
+ LogType["Action"] = "action";
2566
+ // Error
2567
+ LogType["Error"] = "error";
2568
+ })(LogType || (LogType = {}));
2569
+ var LogType$1 = LogType;
2570
+
2571
+ /**
2572
+ * Source of a log event
2573
+ * @author Gabe Abrams
2574
+ */
2575
+ var LogSource;
2576
+ (function (LogSource) {
2577
+ // Client
2578
+ LogSource["Client"] = "client";
2579
+ // Server
2580
+ LogSource["Server"] = "server";
2581
+ })(LogSource || (LogSource = {}));
2582
+ var LogSource$1 = LogSource;
2583
+
2584
+ /**
2585
+ * Built-in metadata for logs
2586
+ * @author Gabe Abrams
2587
+ */
2588
+ const LogBuiltInMetadata = {
2589
+ // Contexts
2590
+ Context: {
2591
+ Uncategorized: 'n/a',
2592
+ ServerRenderedErrorPage: '_server-rendered-error-page',
2593
+ ServerEndpointError: '_server-endpoint-error',
2594
+ },
2595
+ // Targets
2596
+ Target: {
2597
+ NoSpecificTarget: 'n/a',
2598
+ },
2599
+ };
2600
+
2601
+ /**
2602
+ * Types of actions
2603
+ * @author Gabe Abrams
2604
+ */
2605
+ var LogAction;
2606
+ (function (LogAction) {
2607
+ // Target was opened by the user (it was not on screen, but now it is)
2608
+ LogAction["Open"] = "open";
2609
+ // Target was closed by the user (it was on screen, but now it is not)
2610
+ LogAction["Close"] = "close";
2611
+ // Target was cancelled by the user (it was on closed without saving)
2612
+ LogAction["Cancel"] = "cancel";
2613
+ // Target was expanded by the user (it always remains on screen, but size was changed)
2614
+ LogAction["Expand"] = "expand";
2615
+ // Target was collapsed by the user (it always remains on screen, but size was changed)
2616
+ LogAction["Collapse"] = "collapse";
2617
+ // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2618
+ LogAction["View"] = "view";
2619
+ // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2620
+ LogAction["Interrupt"] = "interrupt";
2621
+ // Target was created by the user (it did not exist before)
2622
+ LogAction["Create"] = "create";
2623
+ // Target was edited by the user (it existed and was changed)
2624
+ LogAction["Edit"] = "edit";
2625
+ // Target was deleted by the user (it existed and now it doesn't)
2626
+ LogAction["Delete"] = "delete";
2627
+ // Target was added by the user (it already existed and was added to another place)
2628
+ LogAction["Add"] = "add";
2629
+ // Target was removed by the user (it was removed from something but still exists)
2630
+ LogAction["Remove"] = "remove";
2631
+ // Target was activated by the user (click, check, tap, keypress, etc.)
2632
+ LogAction["Activate"] = "activate";
2633
+ // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2634
+ LogAction["Deactivate"] = "deactivate";
2635
+ // User showed interest in a target (hover, peek, etc.)
2636
+ LogAction["Peek"] = "peek";
2637
+ // Unknown action
2638
+ LogAction["Unknown"] = "unknown";
2639
+ })(LogAction || (LogAction = {}));
2640
+ var LogAction$1 = LogAction;
2641
+
2369
2642
  /**
2370
2643
  * Generate an express API route handler
2371
2644
  * @author Gabe Abrams
@@ -2681,6 +2954,146 @@ const genRouteHandler = (opts) => {
2681
2954
  status: 401,
2682
2955
  });
2683
2956
  }
2957
+ /*----------------------------------------*/
2958
+ /* Log Handler */
2959
+ /*----------------------------------------*/
2960
+ // Create a log handler function
2961
+ /**
2962
+ * Log an event on the server
2963
+ * @author Gabe Abrams
2964
+ */
2965
+ const logServerEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
2966
+ var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
2967
+ // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
2968
+ // that indicates that this is actually a client event, but we don't
2969
+ // include that in the LogFunction type because this is internal and
2970
+ // hidden from users
2971
+ try {
2972
+ // Parse user agent
2973
+ const { browser, device, } = parseUserAgent(req.headers['user-agent']);
2974
+ // Get time info in ET
2975
+ const { timestamp, year, month, day, hour, minute, } = getTimeInfoInET();
2976
+ // Main log info
2977
+ const mainLogInfo = {
2978
+ id: `${launchInfo.userId}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
2979
+ userFirstName: launchInfo.userFirstName,
2980
+ userLastName: launchInfo.userLastName,
2981
+ userEmail: launchInfo.userEmail,
2982
+ userId: launchInfo.userId,
2983
+ isLearner: !!launchInfo.isLearner,
2984
+ isAdmin: !!launchInfo.isAdmin,
2985
+ isTTM: !!launchInfo.isTTM,
2986
+ courseId: launchInfo.courseId,
2987
+ courseName: launchInfo.courseName,
2988
+ browser,
2989
+ device,
2990
+ year,
2991
+ month,
2992
+ day,
2993
+ hour,
2994
+ minute,
2995
+ timestamp,
2996
+ context: (typeof opts.context === 'string'
2997
+ ? opts.context
2998
+ : ((_d = ((_c = opts.context) !== null && _c !== void 0 ? _c : {})._) !== null && _d !== void 0 ? _d : LogBuiltInMetadata.Context.Uncategorized)),
2999
+ subcontext: (typeof opts.context === 'string'
3000
+ ? opts.subcontext
3001
+ : ((_f = ((_e = opts.subcontext) !== null && _e !== void 0 ? _e : {})._) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Context.Uncategorized)),
3002
+ tags: (_g = opts.tags) !== null && _g !== void 0 ? _g : [],
3003
+ metadata: (_h = opts.metadata) !== null && _h !== void 0 ? _h : {},
3004
+ };
3005
+ // Type-specific info
3006
+ const typeSpecificInfo = (('error' in opts && opts.error)
3007
+ ? {
3008
+ type: LogType$1.Error,
3009
+ errorMessage: (_j = opts.error.message) !== null && _j !== void 0 ? _j : 'Unknown message',
3010
+ errorCode: (_k = opts.error.code) !== null && _k !== void 0 ? _k : ReactKitErrorCode$1.NoCode,
3011
+ errorStack: (_l = opts.error.stack) !== null && _l !== void 0 ? _l : 'No stack',
3012
+ }
3013
+ : {
3014
+ type: LogType$1.Action,
3015
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
3016
+ action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
3017
+ });
3018
+ // Source-specific info
3019
+ const sourceSpecificInfo = (opts.overrideAsClientEvent
3020
+ ? {
3021
+ source: LogSource$1.Client,
3022
+ }
3023
+ : {
3024
+ source: LogSource$1.Server,
3025
+ routePath: req.path,
3026
+ routeTemplate: req.route.path,
3027
+ });
3028
+ // Build log event
3029
+ const log = Object.assign(Object.assign(Object.assign({}, mainLogInfo), typeSpecificInfo), sourceSpecificInfo);
3030
+ // Either print to console or save to db
3031
+ const logCollection = internalGetLogCollection();
3032
+ if (logCollection) {
3033
+ // Store to the log collection
3034
+ yield logCollection.insert(log);
3035
+ }
3036
+ else {
3037
+ // Print to console
3038
+ if (log.type === LogType$1.Error) {
3039
+ console.error('dce-reactkit error log:', log);
3040
+ }
3041
+ else {
3042
+ console.log('dce-reactkit action log:', log);
3043
+ }
3044
+ }
3045
+ // Return log entry
3046
+ return log;
3047
+ }
3048
+ catch (err) {
3049
+ // Print because we cannot store the error
3050
+ console.error('Could not log the following:', opts);
3051
+ // Create a dummy log to return
3052
+ const dummyMainInfo = {
3053
+ id: '-1',
3054
+ userFirstName: 'Unknown',
3055
+ userLastName: 'Unknown',
3056
+ userEmail: 'unknown@harvard.edu',
3057
+ userId: 1,
3058
+ isLearner: false,
3059
+ isAdmin: false,
3060
+ isTTM: false,
3061
+ courseId: 1,
3062
+ courseName: 'Unknown',
3063
+ browser: {
3064
+ name: 'Unknown',
3065
+ version: 'Unknown',
3066
+ },
3067
+ device: {
3068
+ isMobile: false,
3069
+ os: 'Unknown',
3070
+ },
3071
+ year: 1,
3072
+ month: 1,
3073
+ day: 1,
3074
+ hour: 1,
3075
+ minute: 1,
3076
+ timestamp: Date.now(),
3077
+ tags: [],
3078
+ metadata: {},
3079
+ context: LogBuiltInMetadata.Context.Uncategorized,
3080
+ subcontext: LogBuiltInMetadata.Context.Uncategorized,
3081
+ };
3082
+ const dummyTypeSpecificInfo = {
3083
+ type: LogType$1.Error,
3084
+ errorMessage: 'Unknown',
3085
+ errorCode: 'Unknown',
3086
+ errorStack: 'No Stack',
3087
+ };
3088
+ const dummySourceSpecificInfo = {
3089
+ source: LogSource$1.Server,
3090
+ routePath: req.path,
3091
+ routeTemplate: req.route.path,
3092
+ };
3093
+ const log = Object.assign(Object.assign(Object.assign({}, dummyMainInfo), dummyTypeSpecificInfo), dummySourceSpecificInfo);
3094
+ return log;
3095
+ }
3096
+ });
2684
3097
  /*------------------------------------------------------------------------*/
2685
3098
  /* Call handler */
2686
3099
  /*------------------------------------------------------------------------*/
@@ -2718,9 +3131,24 @@ const genRouteHandler = (opts) => {
2718
3131
  * @param [opts.status=500] http status code
2719
3132
  */
2720
3133
  const renderErrorPage = (opts = {}) => {
2721
- var _a;
3134
+ var _a, _b;
2722
3135
  const html = genErrorPage(opts);
2723
3136
  send(html, (_a = opts.status) !== null && _a !== void 0 ? _a : 500);
3137
+ // Log
3138
+ logServerEvent({
3139
+ context: LogBuiltInMetadata.Context.ServerRenderedErrorPage,
3140
+ error: {
3141
+ message: `${opts.title}: ${opts.description}`,
3142
+ code: opts.code,
3143
+ },
3144
+ metadata: {
3145
+ title: opts.title,
3146
+ description: opts.description,
3147
+ code: opts.code,
3148
+ pageTitle: opts.pageTitle,
3149
+ status: (_b = opts.status) !== null && _b !== void 0 ? _b : 500,
3150
+ },
3151
+ });
2724
3152
  };
2725
3153
  // Call the handler
2726
3154
  try {
@@ -2734,6 +3162,7 @@ const genRouteHandler = (opts) => {
2734
3162
  },
2735
3163
  redirect,
2736
3164
  renderErrorPage,
3165
+ logServerEvent,
2737
3166
  });
2738
3167
  // Send results to client (only if next wasn't called)
2739
3168
  if (!responseSent) {
@@ -2743,7 +3172,13 @@ const genRouteHandler = (opts) => {
2743
3172
  catch (err) {
2744
3173
  // Send error to client (only if next wasn't called)
2745
3174
  if (!responseSent) {
2746
- return handleError(res, err);
3175
+ handleError(res, err);
3176
+ // Log server-side error
3177
+ logServerEvent({
3178
+ context: LogBuiltInMetadata.Context.ServerEndpointError,
3179
+ error: err,
3180
+ });
3181
+ return;
2747
3182
  }
2748
3183
  // Log error that was not responded with
2749
3184
  console.log('Error occurred but could not be sent to client because a response was already sent:', err);
@@ -2937,6 +3372,61 @@ const parallelLimit = (taskFunctions, limit) => __awaiter(void 0, void 0, void 0
2937
3372
  return results;
2938
3373
  });
2939
3374
 
3375
+ /**
3376
+ * Log a user action on the client (cannot be used on the server)
3377
+ * @author Gabe Abrams
3378
+ */
3379
+ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
3380
+ var _a, _b, _c, _d, _e, _f, _g;
3381
+ return visitServerEndpoint({
3382
+ path: LOG_ROUTE_PATH,
3383
+ method: 'POST',
3384
+ params: {
3385
+ context: (typeof opts.context === 'string'
3386
+ ? opts.context
3387
+ : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
3388
+ subcontext: (typeof opts.context === 'string'
3389
+ ? opts.subcontext
3390
+ : ((_d = ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : {})._) !== null && _d !== void 0 ? _d : LogBuiltInMetadata.Context.Uncategorized)),
3391
+ tags: JSON.stringify((_e = opts.tags) !== null && _e !== void 0 ? _e : []),
3392
+ metadata: JSON.stringify((_f = opts.metadata) !== null && _f !== void 0 ? _f : {}),
3393
+ errorMessage: (opts.error
3394
+ ? opts.error.message
3395
+ : undefined),
3396
+ errorCode: (opts.error
3397
+ ? opts.error.code
3398
+ : undefined),
3399
+ errorStack: (opts.error
3400
+ ? opts.error.stack
3401
+ : undefined),
3402
+ target: (opts.action
3403
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
3404
+ : undefined),
3405
+ action: (opts.action
3406
+ ? opts.action
3407
+ : undefined),
3408
+ },
3409
+ });
3410
+ });
3411
+
3412
+ /**
3413
+ * Initialize a log collection given the dce-mango Collection class
3414
+ * @author Gabe Abrams
3415
+ * @param Collection the Collection class from dce-mango
3416
+ * @returns initialized logCollection
3417
+ */
3418
+ const initLogCollection = (Collection) => {
3419
+ return new Collection('Log', {
3420
+ uniqueIndexKey: 'id',
3421
+ indexKeys: [
3422
+ 'courseId',
3423
+ 'context',
3424
+ 'subcontext',
3425
+ 'tags',
3426
+ ],
3427
+ });
3428
+ };
3429
+
2940
3430
  /**
2941
3431
  * Days of the week
2942
3432
  * @author Gabe Abrams
@@ -2965,6 +3455,10 @@ exports.ErrorWithCode = ErrorWithCode;
2965
3455
  exports.HOUR_IN_MS = HOUR_IN_MS;
2966
3456
  exports.ItemPicker = ItemPicker;
2967
3457
  exports.LoadingSpinner = LoadingSpinner;
3458
+ exports.LogAction = LogAction$1;
3459
+ exports.LogBuiltInMetadata = LogBuiltInMetadata;
3460
+ exports.LogSource = LogSource$1;
3461
+ exports.LogType = LogType$1;
2968
3462
  exports.MINUTE_IN_MS = MINUTE_IN_MS;
2969
3463
  exports.Modal = Modal;
2970
3464
  exports.ModalButtonType = ModalButtonType$1;
@@ -2993,7 +3487,9 @@ exports.getPartOfDay = getPartOfDay;
2993
3487
  exports.getTimeInfoInET = getTimeInfoInET;
2994
3488
  exports.handleError = handleError;
2995
3489
  exports.handleSuccess = handleSuccess;
3490
+ exports.initLogCollection = initLogCollection;
2996
3491
  exports.initServer = initServer;
3492
+ exports.logClientEvent = logClientEvent;
2997
3493
  exports.onlyKeepLetters = onlyKeepLetters;
2998
3494
  exports.padDecimalZeros = padDecimalZeros;
2999
3495
  exports.padZerosLeft = padZerosLeft;