Merge pull request #1789 from Safelite/feature/CSR-32-A

Feature/csr 32 a
This commit is contained in:
Leah Schumann 2024-03-05 08:39:15 -05:00 committed by GitHub
commit abcfa1d11a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 189 additions and 31 deletions

View file

@ -1,8 +1,8 @@
<template>
<router-view v-slot="{ Component }">
<router-view v-slot="{ Component }" ref="router-view">
<transition :duration="{ enter: 200, leave: 200 }" name="route-fade" mode="out-in">
<!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" -->
<component :is="Component" @focusin="handleAnyComponentFocus" />
<component :is="Component" ref="component" @focusin="handleAnyComponentFocus" />
</transition>
</router-view>
<loadingModal :showLoader="shouldShowLoader" :showTextCarousel="shouldShowTextCarousel" />
@ -12,6 +12,7 @@
import { handleAnyComponentFocus } from "@/helpers/button-question-focus-helper";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
export default {
name: "app",
setup() {
@ -33,6 +34,24 @@ export default {
loadingModal,
},
};
// Add a global Javascript exception handler for logging exceptions, this handler will log when there is an uncaught exception
window.onerror = (msg, url, line, col, error) => {
// Log the windows error
// Note that col & error are new to the HTML 5 spec and may not be
// supported in every browser. It worked for me in Chrome.
global.$logger.logError(msg, {
url: url,
line: line,
column: col ?? "",
error: error ?? "",
});
// If you return true, then error alerts (like in older versions of
// Internet Explorer) will be suppressed.
var suppressErrorAlert = true;
return suppressErrorAlert;
};
</script>
<style lang="scss">

View file

@ -28,6 +28,7 @@ const applicationConfig = {
YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60",
OUTLOOK_CALENDAR:
"https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent",
FRONTEND_LOGGER_URL: process.env.VUE_APP_CONSUMER_CF_DISTRO + "/analytics/api/v1/logging",
};
export { applicationConfig };

View file

@ -0,0 +1,11 @@
const axiosResponseInterceptorMessages = {
NETWORK_ERROR:
"A network error occurred. " +
"This could be a bad URL, a CORS issue, or a dropped internet connection. " +
"It is impossible for us to know.",
STATUS_CODE_ERROR: "Status Code Error",
NO_RESPONSE_ERROR: "The request was made but no response was received",
GENERIC_ERROR: "Error",
};
export default axiosResponseInterceptorMessages;

View file

@ -0,0 +1,8 @@
const loggingEndpointMethods = {
LOG_INFORMATION: "log-information",
LOG_WARNING: "log-warning",
LOG_ERROR: "log-error",
LOG_CRITICAL: "log-critical",
};
export default loggingEndpointMethods;

View file

@ -1,11 +1,50 @@
import axios from "axios";
import analyticsMixIn from "@/mixins/analytics-mixin.js";
import store from "@/store";
import router from "@/router";
import store from "@/store";
import { applicationConfig } from "@/constants/application-config.js";
import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
import { headerKeys } from "@/constants/header-keys";
import axiosResponseInterceptorMessages from "@/constants/axios-response-interceptor-messages.js";
// Add a response interceptor for global axios error handing.
axios.interceptors.response.use(
(response) => response,
(error) => {
let rejectionError = "";
if (typeof error.response === "undefined") {
// The request was not made, could be a bad url, bad connection, or a CORS error.
rejectionError = {
response: error,
message: axiosResponseInterceptorMessages.NETWORK_ERROR,
};
} else if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
rejectionError = {
response: error.response,
message: axiosResponseInterceptorMessages.STATUS_CODE_ERROR,
};
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
rejectionError = {
response: error.request,
message: axiosResponseInterceptorMessages.NO_RESPONSE_ERROR,
};
} else {
// Something happened in setting up the request that triggered an Error
rejectionError = {
response: error.message,
message: axiosResponseInterceptorMessages.GENERIC_ERROR,
};
}
return Promise.reject(rejectionError);
}
);
export default {
callHttpClient({
@ -50,8 +89,6 @@ export default {
return resolve(response);
},
(error) => {
console.error(error);
if (logApiCall) {
analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE,
@ -61,11 +98,16 @@ export default {
);
}
// do not route to error logic when no wipers found or no promo found (404s)
// Do not route to error logic when no wipers found or no promo found (404s)
if (error.response.status != "404") {
router.navigateError();
}
global.$logger.logError(
`${method}: ${endpoint}: ${error.message}`,
error.response
);
return reject(error.response);
}
);

View file

@ -7,6 +7,13 @@ import router from "@/router";
jest.mock("axios");
jest.mock("@/mixins/analytics-mixin");
global.$logger = {
logInformation: jest.fn(),
logWarning: jest.fn(),
logError: jest.fn(),
logCritical: jest.fn(),
};
it("Global Methods - Call Http Client - Should Resolve Promise", () => {
//Arrange
const endpoint = "https://mock.safelite.com";

74
src/helpers/logger.js Normal file
View file

@ -0,0 +1,74 @@
import { applicationConfig } from "@/constants/application-config.js";
import axios from "axios";
import store from "@/store";
import loggingEndpointMethods from "@/constants/logging-endpoint-methods";
import { headerKeys } from "@/constants/header-keys";
export class Logger {
logInformation(message, details) {
this.writeLogEntry(
loggingEndpointMethods.LOG_INFORMATION,
this.formatLogEntry(message, details)
);
}
logWarning(message, details) {
this.writeLogEntry(
loggingEndpointMethods.LOG_WARNING,
this.formatLogEntry(message, details)
);
}
logError(message, details) {
this.writeLogEntry(loggingEndpointMethods.LOG_ERROR, this.formatLogEntry(message, details));
}
logCritical(message, details) {
this.writeLogEntry(
loggingEndpointMethods.LOG_CRITICAL,
this.formatLogEntry(message, details)
);
}
formatLogEntry(message, details) {
return `Application: ${applicationConfig.APPLICATION_NAME}\n${message}\n${
details ? JSON.stringify(details, undefined, 2) : ""
}`;
}
writeLogEntry(endpoint, logEntry) {
return new Promise((resolve, reject) => {
// If running locally or in the Dev environment show the log entries in the console.
if (
applicationConfig.CURRENT_ENVIRONMENT === "Localhost" ||
applicationConfig.CURRENT_ENVIRONMENT === "Dev"
) {
console.log(logEntry);
}
const url = applicationConfig.FRONTEND_LOGGER_URL;
const headers = {
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings),
};
axios({
method: "POST",
url: `${url}/${endpoint}`,
data: { entry: logEntry },
crossDomain: true,
responseType: {},
headers: headers,
}).then(
(response) => {
return resolve(response);
},
(error) => {
return reject(error);
}
);
});
}
}
export default Logger;

View file

@ -8,6 +8,10 @@ import baseMixin from "@/mixins/base-mixin.js";
import analyticsMixin from "@/mixins/analytics-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import "../node_modules/bootstrap/dist/js/bootstrap.js";
import Logger from "@/helpers/logger";
// Instantiate global logging object
global.$logger = new Logger();
// Vue App Setup
const vueApp = createApp(App);
@ -20,4 +24,11 @@ vueApp.mixin(baseMixin);
vueApp.mixin(analyticsMixin);
vueApp.mixin(experimentMixin);
// Vue Error Handling
vueApp.config.errorHandler = (err, vm, info) => {
global.$logger.logError(
`Page Name - ${vm.getPageName()} - ${info}: ${err.message}\n${err.stack}`
);
};
vueApp.mount("#app");

View file

@ -152,7 +152,7 @@ const routes = [
params: to.params,
});
} catch (error) {
console.log(error);
global.$logger.logError(`Routing ${error.stack}`);
if (to.query?.fmgPage === funnelStartPageName) {
deleteFunnelCookie();

View file

@ -1126,14 +1126,9 @@ export const actions = {
payload: payload,
logApiCall: false,
})
.then(
(response) => {
return response;
},
(error) => {
console.log("Analytics Service Error: " + error.data);
}
);
.then((response) => {
return response;
});
},
logCustomEvent(
@ -1180,14 +1175,9 @@ export const actions = {
payload: payload,
logApiCall: false,
})
.then(
(response) => {
return response;
},
(error) => {
console.log("Analytics Service Error: " + error.data);
}
);
.then((response) => {
return response;
});
},
initializeSession(context, { userId, deviceId, sessionId, userAgent, referrer }) {
var payload = {
@ -1208,14 +1198,9 @@ export const actions = {
payload: payload,
logApiCall: false,
})
.then(
(response) => {
return response;
},
(error) => {
console.log("Analytics Service Error: " + error.data);
}
);
.then((response) => {
return response;
});
},
// Misc Actions