@compilacion/colleciones-clientos 2.0.28 → 2.0.30

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.
@@ -10,12 +10,13 @@ var collecionesClientosGtmConfigSandbox = (function (exports) {
10
10
  'trackerName': 'trackerName',
11
11
  'appName': 'appName',
12
12
  'emitterEndpoint': 'emitterEndpoint',
13
+ 'errorLoggingEndpoint': 'errorLoggingEndpoint',
13
14
  'typeName': 'type'
14
15
  },
15
16
  'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'
16
17
  };
17
18
 
18
- function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput) {
19
+ function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput, errorLoggingEndpoint) {
19
20
  let returnObject = {};
20
21
  returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;
21
22
  returnObject[constants.config.emitterEndpoint] = endpoint;
@@ -25,9 +26,12 @@ var collecionesClientosGtmConfigSandbox = (function (exports) {
25
26
  returnObject[constants.config.appName] = appName;
26
27
  returnObject[constants.config.sourceName] = jsLibSource;
27
28
  returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;
29
+ // Optional: providing the URL of the server-side error-logging endpoint is what
30
+ // turns frontend error forwarding on. When left empty the runtime stays silent.
31
+ returnObject[constants.config.errorLoggingEndpoint] = errorLoggingEndpoint;
28
32
  return returnObject;
29
33
  }
30
- /* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl);*/
34
+ /* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl, errorLoggingEndpoint);*/
31
35
 
32
36
  exports.getConfigObject = getConfigObject;
33
37
 
@@ -1 +1 @@
1
- {"version":3,"file":"browser.domainModel.gtm.config.sandbox.js","sources":["../src/browser.domainModel.gtm.constants.js","../src/browser.domainModel.gtm.config.sandbox.js"],"sourcesContent":["var constants = {\n 'sharedWindowObjectNames': {\n 'queue': 'compilacionCollecionesClientosGtmQueue',\n 'configVariable': 'compilacionCollecionesClientosGtmTypeCastConfig'\n },\n 'config': {\n 'sourceName': 'jsLibSource',\n 'jsLibUrlVariable': 'jsLibSource_selfHostedUrl',\n 'selfHostedValue': 'SELF',\n 'unpkgHostedValue': 'UNPKG',\n 'flushInterval': 'flushInterval',\n 'flushSize': 'flushSize',\n 'trackerName': 'trackerName',\n 'appName': 'appName',\n 'emitterEndpoint': 'emitterEndpoint',\n 'typeName': 'type'\n },\n 'collecionesObject': 'compilacionCollecionesClientosGtm',\n 'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'\n};\n\nexport default constants;","import constants from './browser.domainModel.gtm.constants.js';\n\nexport function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput) { \n let jsLibUrl = null;\n if(jsLibSource === constants.config.unpkgHostedValue) {\n jsLibUrl = \"https://unpkg.com/@compilacion/colleciones-clientos@latest/dist/browser.gtm.min.js\";\n } else if (jsLibSource === constants.config.selfHostedValue) {\n jsLibUrl = jsLibUrlInput\n }\n let returnObject = {};\n returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;\n returnObject[constants.config.emitterEndpoint] = endpoint;\n returnObject[constants.config.flushInterval] = flushInterval;\n returnObject[constants.config.flushSize] = flushSize;\n returnObject[constants.config.trackerName] = trackerName;\n returnObject[constants.config.appName] = appName;\n returnObject[constants.config.sourceName] = jsLibSource;\n returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;\n return returnObject;\n}\n/* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl);*/"],"names":[],"mappings":";;;IAAA,IAAI,SAAS,GAAG;IAChB,IAII,QAAQ,EAAE;IACd,QAAQ,YAAY,EAAE,aAAa;IACnC,QAAQ,kBAAkB,EAAE,2BAA2B;IACvD,QAEQ,eAAe,EAAE,eAAe;IACxC,QAAQ,WAAW,EAAE,WAAW;IAChC,QAAQ,aAAa,EAAE,aAAa;IACpC,QAAQ,SAAS,EAAE,SAAS;IAC5B,QAAQ,iBAAiB,EAAE,iBAAiB;IAC5C,QAAQ,UAAU,EAAE;IACpB,KAAK;IACL,IACI,2BAA2B,EAAE;IACjC,CAAC;;ICjBM,SAAS,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE;IAOtH,IAAI,IAAI,YAAY,GAAG,EAAE;IACzB,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,yBAAyB;IACjF,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,QAAQ;IAC7D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,aAAa;IAChE,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS;IACxD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,WAAW;IAC5D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO;IACpD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,WAAW;IAC3D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,aAAa;IACnE,IAAI,OAAO,YAAY;IACvB;IACA;;;;;;;;;;"}
1
+ {"version":3,"file":"browser.domainModel.gtm.config.sandbox.js","sources":["../src/browser.domainModel.gtm.constants.js","../src/browser.domainModel.gtm.config.sandbox.js"],"sourcesContent":["var constants = {\n 'sharedWindowObjectNames': {\n 'queue': 'compilacionCollecionesClientosGtmQueue',\n 'configVariable': 'compilacionCollecionesClientosGtmTypeCastConfig'\n },\n 'config': {\n 'sourceName': 'jsLibSource',\n 'jsLibUrlVariable': 'jsLibSource_selfHostedUrl',\n 'selfHostedValue': 'SELF',\n 'unpkgHostedValue': 'UNPKG',\n 'flushInterval': 'flushInterval',\n 'flushSize': 'flushSize',\n 'trackerName': 'trackerName',\n 'appName': 'appName',\n 'emitterEndpoint': 'emitterEndpoint',\n 'errorLoggingEndpoint': 'errorLoggingEndpoint',\n 'typeName': 'type'\n },\n 'collecionesObject': 'compilacionCollecionesClientosGtm',\n 'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'\n};\n\nexport default constants;","import constants from './browser.domainModel.gtm.constants.js';\n\nexport function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput, errorLoggingEndpoint) {\n let jsLibUrl = null;\n if(jsLibSource === constants.config.unpkgHostedValue) {\n jsLibUrl = \"https://unpkg.com/@compilacion/colleciones-clientos@latest/dist/browser.gtm.min.js\";\n } else if (jsLibSource === constants.config.selfHostedValue) {\n jsLibUrl = jsLibUrlInput\n }\n let returnObject = {};\n returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;\n returnObject[constants.config.emitterEndpoint] = endpoint;\n returnObject[constants.config.flushInterval] = flushInterval;\n returnObject[constants.config.flushSize] = flushSize;\n returnObject[constants.config.trackerName] = trackerName;\n returnObject[constants.config.appName] = appName;\n returnObject[constants.config.sourceName] = jsLibSource;\n returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;\n // Optional: providing the URL of the server-side error-logging endpoint is what\n // turns frontend error forwarding on. When left empty the runtime stays silent.\n returnObject[constants.config.errorLoggingEndpoint] = errorLoggingEndpoint;\n return returnObject;\n}\n/* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl, errorLoggingEndpoint);*/"],"names":[],"mappings":";;;IAAA,IAAI,SAAS,GAAG;IAChB,IAII,QAAQ,EAAE;IACd,QAAQ,YAAY,EAAE,aAAa;IACnC,QAAQ,kBAAkB,EAAE,2BAA2B;IACvD,QAEQ,eAAe,EAAE,eAAe;IACxC,QAAQ,WAAW,EAAE,WAAW;IAChC,QAAQ,aAAa,EAAE,aAAa;IACpC,QAAQ,SAAS,EAAE,SAAS;IAC5B,QAAQ,iBAAiB,EAAE,iBAAiB;IAC5C,QAAQ,sBAAsB,EAAE,sBAAsB;IACtD,QAAQ,UAAU,EAAE;IACpB,KAAK;IACL,IACI,2BAA2B,EAAE;IACjC,CAAC;;IClBM,SAAS,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,oBAAoB,EAAE;IAO5I,IAAI,IAAI,YAAY,GAAG,EAAE;IACzB,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,yBAAyB;IACjF,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,QAAQ;IAC7D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,aAAa;IAChE,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS;IACxD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,WAAW;IAC5D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO;IACpD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,WAAW;IAC3D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,aAAa;IACnE;IACA;IACA,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB;IAC9E,IAAI,OAAO,YAAY;IACvB;IACA;;;;;;;;;;"}
@@ -791,7 +791,8 @@
791
791
  'flushSize': 'flushSize',
792
792
  'trackerName': 'trackerName',
793
793
  'appName': 'appName',
794
- 'emitterEndpoint': 'emitterEndpoint'},
794
+ 'emitterEndpoint': 'emitterEndpoint',
795
+ 'errorLoggingEndpoint': 'errorLoggingEndpoint'},
795
796
  'collecionesObject': 'compilacionCollecionesClientosGtm',
796
797
  'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'
797
798
  };
@@ -808,6 +809,62 @@
808
809
  let queueReference = null;
809
810
  let processedQueueLength = 0;
810
811
 
812
+ // Identity/destination details captured from the GTM config so a caught error
813
+ // can be attributed (tracker/app) and forwarded to the right endpoint.
814
+ let appName = null;
815
+ let trackerName = null;
816
+ let errorLoggingEndpoint = null;
817
+
818
+ const safeStringify = (value) => {
819
+ if (value === undefined) return '';
820
+ if (typeof value === 'string') return value;
821
+ try {
822
+ return JSON.stringify(value);
823
+ } catch (e) {
824
+ return String(value);
825
+ }
826
+ };
827
+
828
+ // Forward a caught client-side error to the configured bad-event endpoint.
829
+ // The whole point of this is to make the try/catch fallbacks visible server-side,
830
+ // so it is strictly best-effort and must NEVER throw: a failure to report an
831
+ // error can not be allowed to break the tracking flow it is meant to protect.
832
+ // Reporting only happens when an errorLoggingEndpoint URL is configured.
833
+ const reportError = (error, details = {}) => {
834
+ if (!errorLoggingEndpoint) return;
835
+ if (typeof fetch !== 'function') return;
836
+ try {
837
+ const payload = {
838
+ errorName: (error && error.name) ? String(error.name) : 'Error',
839
+ errorMessage: (error && error.message) ? String(error.message) : String(error),
840
+ tracker: trackerName != null ? String(trackerName) : '',
841
+ appName: appName != null ? String(appName) : '',
842
+ entity: details.entity != null ? String(details.entity) : '',
843
+ action: details.action != null ? String(details.action) : '',
844
+ rawEvent: safeStringify(details.rawEvent),
845
+ context: {
846
+ // phase identifies which fallback caught the error: init, flush or group.
847
+ phase: details.phase || '',
848
+ stack: (error && error.stack) ? String(error.stack) : '',
849
+ url: (typeof window !== 'undefined' && window.location) ? window.location.href : '',
850
+ userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : '',
851
+ language: (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : '',
852
+ clientTimestamp: new Date().toISOString()
853
+ }
854
+ };
855
+ // keepalive lets a report fired during page unload still have a chance to land.
856
+ fetch(errorLoggingEndpoint, {
857
+ method: 'POST',
858
+ credentials: 'include',
859
+ keepalive: true,
860
+ headers: { 'Content-Type': 'application/json' },
861
+ body: JSON.stringify(payload)
862
+ }).catch(() => {});
863
+ } catch (e) {
864
+ // Swallow: error reporting can never be allowed to surface its own exception.
865
+ }
866
+ };
867
+
811
868
  let init = () => {
812
869
  if (initialized) return true;
813
870
  if (window[constants.sharedWindowObjectNames.configVariable] === undefined) return false;
@@ -815,8 +872,11 @@
815
872
  if (config.type === undefined || config.type !== constants.typeCastGtmVariableConfig) return false;
816
873
  let flushInterval = config[constants.config.flushInterval];
817
874
  let flushSize = config[constants.config.flushSize];
818
- let trackerName = config[constants.config.trackerName];
819
- let appName = config[constants.config.appName];
875
+ trackerName = config[constants.config.trackerName];
876
+ appName = config[constants.config.appName];
877
+ // Capture the error endpoint as soon as a valid config is available so even an
878
+ // emitter-init failure below can already be reported.
879
+ errorLoggingEndpoint = config[constants.config.errorLoggingEndpoint] ?? null;
820
880
  let emitterEndpoint = config[constants.config.emitterEndpoint] ?? config.endpoint;
821
881
  try {
822
882
  emitter = new CollecionesEmitter(emitterEndpoint, flushSize, flushInterval);
@@ -825,6 +885,7 @@
825
885
  return true;
826
886
  } catch (e) {
827
887
  console.error('Error initializing CollecionesEmitter:', e);
888
+ reportError(e, { phase: 'init' });
828
889
  return false;
829
890
  }
830
891
  };
@@ -835,6 +896,15 @@
835
896
  if (!tracker) return false;
836
897
  if (!arg || typeof arg !== 'object') return false;
837
898
 
899
+ // GTM variables can resolve to numbers/booleans or be left empty. Coerce
900
+ // entity/action/actor names to a string so a mistyped or empty config value
901
+ // cannot throw inside CollecionesEvent (formatToCamelCase) and drop the event.
902
+ const toEventName = (value) => {
903
+ if (typeof value === 'string') return value;
904
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
905
+ return '';
906
+ };
907
+
838
908
  const normalizeIdentifiers = (identifiers) => {
839
909
  if (Array.isArray(identifiers)) {
840
910
  return identifiers.flatMap(identifier => {
@@ -970,24 +1040,27 @@
970
1040
  };
971
1041
 
972
1042
  const populateEvent = (eventArg, event) => {
973
- event.setEntity(eventArg.entity);
974
- event.setAction(eventArg.action);
1043
+ event.setEntity(toEventName(eventArg.entity));
1044
+ event.setAction(toEventName(eventArg.action));
975
1045
 
976
1046
  normalizeIdentifiers(eventArg.identifiers).forEach(identifier => {
977
1047
  if (typeof identifier !== 'object' || identifier === null) return;
978
- if (!identifier.name || !identifier.value) return;
1048
+ // Reject only an absent name or a null/undefined value; falsy-but-valid
1049
+ // values such as 0 or '' must still be kept (matches addCollectionItem).
1050
+ if (!identifier.name || identifier.value == null) return;
979
1051
  event.setIdentifier(identifier.name, identifier.value);
980
1052
  });
981
1053
 
982
- if (eventArg.actor?.entity) {
983
- event.setActor(eventArg.actor.entity);
1054
+ const actorName = toEventName(eventArg.actor?.entity ?? eventArg.actor?.name);
1055
+ if (actorName) {
1056
+ event.setActor(actorName);
984
1057
  let identifiers = eventArg.actor?.identifiers;
985
1058
  if (!Array.isArray(identifiers)) {
986
- identifiers = identifiers?.[eventArg.actor.entity] ?? identifiers;
1059
+ identifiers = identifiers?.[actorName] ?? identifiers;
987
1060
  }
988
1061
  normalizeIdentifiers(identifiers).forEach(identifier => {
989
1062
  if (typeof identifier !== 'object' || identifier === null) return;
990
- if (!identifier.name || !identifier.value) return;
1063
+ if (!identifier.name || identifier.value == null) return;
991
1064
  event.setActorIdentifier(identifier.name, identifier.value);
992
1065
  });
993
1066
  }
@@ -995,15 +1068,30 @@
995
1068
  const relations = eventArg?.relations ?? eventArg?.rerelations;
996
1069
  if (relations && typeof relations === 'object') {
997
1070
  Object.keys(relations).forEach(relationName => {
1071
+ const relationIdentifiers = relations[relationName];
998
1072
  event.setReference(relationName);
999
- Object.keys(relations[relationName]).forEach(identifierName => {
1000
- event.setReferenceIdentifier(relationName, identifierName, relations[relationName][identifierName]);
1001
- });
1073
+ // A relation can be declared while its identifier object is left empty
1074
+ // (an unfilled GTM variable resolves to undefined/null). Guard against
1075
+ // non-object values so Object.keys cannot throw and drop the event.
1076
+ if (relationIdentifiers && typeof relationIdentifiers === 'object') {
1077
+ Object.keys(relationIdentifiers).forEach(identifierName => {
1078
+ event.setReferenceIdentifier(relationName, identifierName, relationIdentifiers[identifierName]);
1079
+ });
1080
+ }
1002
1081
  });
1003
1082
  }
1004
- if (eventArg?.adjectives && typeof eventArg.adjectives === 'object') {
1083
+ // Adjectives may arrive as an array of values or as an object keyed by adjective.
1084
+ if (Array.isArray(eventArg?.adjectives)) {
1085
+ eventArg.adjectives.forEach(adjective => {
1086
+ if (typeof adjective === 'string' && adjective.length > 0) {
1087
+ event.addAdjective(adjective);
1088
+ }
1089
+ });
1090
+ } else if (eventArg?.adjectives && typeof eventArg.adjectives === 'object') {
1005
1091
  Object.keys(eventArg.adjectives).forEach(adjectiveName => {
1006
- event.addAdjective(adjectiveName, eventArg.adjectives[adjectiveName]);
1092
+ if (adjectiveName.length > 0) {
1093
+ event.addAdjective(adjectiveName);
1094
+ }
1007
1095
  });
1008
1096
  }
1009
1097
 
@@ -1022,10 +1110,23 @@
1022
1110
  let added = 0;
1023
1111
  arg.events.forEach(eventArg => {
1024
1112
  if (!eventArg || typeof eventArg !== 'object') return;
1025
- if (!eventArg.entity || !eventArg.action) return;
1026
- const event = group.addEvent();
1027
- populateEvent(eventArg, event);
1028
- added++;
1113
+ if (!toEventName(eventArg.entity) || !toEventName(eventArg.action)) return;
1114
+ // Build the event before registering it so a single malformed event cannot
1115
+ // throw and take the whole group (including its valid siblings) down with it.
1116
+ try {
1117
+ const event = new CollecionesEvent();
1118
+ populateEvent(eventArg, event);
1119
+ group.addEvent(event);
1120
+ added++;
1121
+ } catch (e) {
1122
+ console.error('Colleciones: skipped malformed grouped event', e);
1123
+ reportError(e, {
1124
+ phase: 'group',
1125
+ rawEvent: eventArg,
1126
+ entity: eventArg?.entity,
1127
+ action: eventArg?.action
1128
+ });
1129
+ }
1029
1130
  });
1030
1131
  if (added === 0) return false;
1031
1132
  // Apply the semantic group name AFTER addEvent calls: each addEvent invalidates
@@ -1037,14 +1138,14 @@
1037
1138
  return true;
1038
1139
  }
1039
1140
 
1040
- if (!arg.entity || !arg.action) return false;
1141
+ if (!toEventName(arg.entity) || !toEventName(arg.action)) return false;
1041
1142
  const event = new CollecionesEvent();
1042
1143
  populateEvent(arg, event);
1043
1144
  tracker.track(event);
1044
1145
  return true;
1045
1146
  };
1046
1147
 
1047
- let flush = () => {
1148
+ let flushInternal = () => {
1048
1149
  if (!init()) {
1049
1150
  return;
1050
1151
  }
@@ -1063,7 +1164,35 @@
1063
1164
  while (processedQueueLength < queue.length) {
1064
1165
  const args = queue[processedQueueLength];
1065
1166
  processedQueueLength += 1;
1066
- track(args);
1167
+ // Never let one bad queue entry abort the flush loop: the index has already
1168
+ // advanced, so an uncaught throw here would permanently skip the rest of the batch.
1169
+ try {
1170
+ track(args);
1171
+ } catch (e) {
1172
+ console.error('Colleciones: failed to process queued event', e);
1173
+ reportError(e, {
1174
+ phase: 'flush',
1175
+ rawEvent: args,
1176
+ entity: args?.entity,
1177
+ action: args?.action
1178
+ });
1179
+ }
1180
+ }
1181
+ };
1182
+
1183
+ // Overarching safety net. The per-event / per-group catches above exist for
1184
+ // RECOVERY (they keep the loop alive), so they intentionally sit inside the loop.
1185
+ // This wrapper catches everything else our own runtime can throw — init, queue
1186
+ // bookkeeping, or any path we forgot to guard — so nothing fails silently again.
1187
+ // It only fires for genuinely unhandled throws (the inner catches swallow theirs),
1188
+ // so there is no double reporting. Scope is our runtime only: page-level errors
1189
+ // from other scripts are deliberately not captured here.
1190
+ let flush = () => {
1191
+ try {
1192
+ flushInternal();
1193
+ } catch (e) {
1194
+ console.error('Colleciones: unexpected runtime failure', e);
1195
+ reportError(e, { phase: 'runtime' });
1067
1196
  }
1068
1197
  };
1069
1198