diff --git a/packages/apps/flash-courier/lib-mjs/update-tracking.mjs b/packages/apps/flash-courier/lib-mjs/update-tracking.mjs index 2d8920617..746598213 100644 --- a/packages/apps/flash-courier/lib-mjs/update-tracking.mjs +++ b/packages/apps/flash-courier/lib-mjs/update-tracking.mjs @@ -12,6 +12,11 @@ export default () => { 'applications?limit=1&fields=hidden_data,data' + `&app_id=${config.get().apps.flashCourier.appId}`, )).data.result; + // App not installed on this store, nothing to track + if (!app[0]) { + resolve(null); + return; + } appData = { ...app[0].data, diff --git a/packages/apps/frenet/src/functions-lib/tracking-codes.ts b/packages/apps/frenet/src/functions-lib/tracking-codes.ts index 7847e3727..b50d34037 100644 --- a/packages/apps/frenet/src/functions-lib/tracking-codes.ts +++ b/packages/apps/frenet/src/functions-lib/tracking-codes.ts @@ -12,7 +12,8 @@ const getConfig = async () => { `applications?app_id=${config.get().apps.frenet.appId}&fields=hidden_data`, )).data.result; - return app[0].hidden_data; + // App not installed on this store, nothing to track + return app[0]?.hidden_data || null; } catch (err) { logger.error(err); return null; @@ -50,8 +51,9 @@ const handleTrackingCodes = async () => { // no config for store bye resolve(null); } else { - // eslint-disable-next-line consistent-return - codes.forEach(async (code) => { + // Wait for every code before finishing, the function may be stopped + // right after resolving + await Promise.allSettled(codes.map(async (code) => { const serviceCode = String(code.serviceCode).startsWith('FR') ? String(code.serviceCode.replace(/^FR/, '')).trim() : code.serviceCode; @@ -144,9 +146,8 @@ const handleTrackingCodes = async () => { } } } - - resolve(null); - }); + })); + resolve(null); } }); diff --git a/packages/apps/melhor-envio/lib-mjs/functions/error-handling.mjs b/packages/apps/melhor-envio/lib-mjs/functions/error-handling.mjs index f8cc34b2c..8b3b91572 100644 --- a/packages/apps/melhor-envio/lib-mjs/functions/error-handling.mjs +++ b/packages/apps/melhor-envio/lib-mjs/functions/error-handling.mjs @@ -1,4 +1,4 @@ -import * as logger from 'firebase-functions/logger'; +import { logger } from '@cloudcommerce/firebase/lib/config'; const ignoreError = (response) => { // check response status code @@ -53,8 +53,8 @@ export default (err) => { } // debug unexpected response error.configJSON = { - originalRequest: JSON.stringify(err.config), - logRequest: JSON.stringify(config), + originalRequest: JSON.stringify({ ...err.config, headers: undefined }), + logRequest: JSON.stringify({ ...config, headers: undefined }), }; logger.error(error); } diff --git a/packages/apps/melhor-envio/src/functions-lib/tracking-codes.ts b/packages/apps/melhor-envio/src/functions-lib/tracking-codes.ts index d553d4cea..fb3cffa86 100644 --- a/packages/apps/melhor-envio/src/functions-lib/tracking-codes.ts +++ b/packages/apps/melhor-envio/src/functions-lib/tracking-codes.ts @@ -12,7 +12,8 @@ const getConfig = async () => { `applications?app_id=${config.get().apps.melhorEnvio.appId}&fields=hidden_data`, )).data.result; - return app[0].hidden_data; + // App not installed on this store, nothing to track + return app[0]?.hidden_data || null; } catch (err) { logger.error(err); return null; diff --git a/packages/firebase/src/config.ts b/packages/firebase/src/config.ts index 5b801d965..a6fc2cfea 100644 --- a/packages/firebase/src/config.ts +++ b/packages/firebase/src/config.ts @@ -250,7 +250,62 @@ export default config; export const asyncLocalStorage = new AsyncLocalStorage<{ execId: string }>(); -const log = (level: 'info' | 'warn' | 'error', msg: any, d?: Record) => { +const secretHeaderRegex = /^(authorization|access-token|x-access-token|x-api-key|api-key|token|cookie)$/i; + +const maskHeaders = (headers: any) => { + if (!headers || typeof headers !== 'object') return headers; + const masked = { ...(typeof headers.toJSON === 'function' ? headers.toJSON() : headers) }; + Object.keys(masked).forEach((name) => { + if (secretHeaderRegex.test(name)) masked[name] = '***'; + }); + return masked; +}; + +// Keep credentials sent on request headers (e.g. Axios error config) out of logs +const maskSecrets = (value: any, depth = 0): any => { + if (!value || typeof value !== 'object' || depth > 3) return value; + if (value.isAxiosError) { + const err = Object.create( + Object.getPrototypeOf(value), + Object.getOwnPropertyDescriptors(value), + ); + // Raw HTTP request has the headers on a plain string + delete err.request; + if (err.config) err.config = { ...err.config, headers: maskHeaders(err.config.headers) }; + if (err.response) { + const { + status, statusText, headers, data, + } = err.response; + err.response = { + status, statusText, headers, data, + }; + } + return err; + } + if (Array.isArray(value)) { + return value.map((item) => maskSecrets(item, depth + 1)); + } + const proto = Object.getPrototypeOf(value); + // Plain objects only, Axios config may have no prototype + if (proto !== Object.prototype && proto !== null) return value; + const masked: Record = {}; + Object.keys(value).forEach((key) => { + masked[key] = key === 'headers' + ? maskHeaders(value[key]) + : maskSecrets(value[key], depth + 1); + }); + return masked; +}; + +const log = (level: 'info' | 'warn' | 'error', _msg: any, _d?: Record) => { + let msg = _msg; + let d = _d; + try { + msg = maskSecrets(_msg); + d = maskSecrets(_d); + } catch { + // Unexpected object (e.g. throwing getter), log as received + } const execId = asyncLocalStorage.getStore()?.execId; if (execId) { if (d) { diff --git a/packages/ssr/src/lib/util/ssr-utils.ts b/packages/ssr/src/lib/util/ssr-utils.ts index c885170fe..36a0ba222 100644 --- a/packages/ssr/src/lib/util/ssr-utils.ts +++ b/packages/ssr/src/lib/util/ssr-utils.ts @@ -41,7 +41,7 @@ type CachedRequestInit = RequestInit & { timeout?: number, }; const runtimeCache: Record = {}; -const runtimeFetchings: Record | undefined> = {}; +const runtimeFetchings: Record | undefined> = {}; // Also declared at `storefront/server.d.ts` export const fetchAndCache = async ( @@ -61,7 +61,7 @@ export const fetchAndCache = async ( return runtimeCache[key].data; } const docRef = getFirestore().doc(`ssrFetchCache/${key}`); - const runFetch = async () => { + const doFetch = async () => { let abortController: AbortController | undefined; let timer: NodeJS.Timeout | undefined; if (timeout) { @@ -71,12 +71,17 @@ export const fetchAndCache = async ( }, timeout); reqInit.signal = abortController.signal; } - const fetching = runtimeFetchings[key] || fetch(url, reqInit); - runtimeFetchings[key] = fetching; - const response = await fetching; - delete runtimeFetchings[key]; - if (timer) { - clearTimeout(timer); + let response: Response; + try { + response = await fetch(url, reqInit); + } catch (err: any) { + // Network error or aborted by timeout + err.url = url; + throw err; + } finally { + if (timer) { + clearTimeout(timer); + } } const error = (err: any) => { err.url = url; @@ -105,6 +110,18 @@ export const fetchAndCache = async ( } return error(new Error(`Failed fetching ${url}`)); }; + // Concurrent calls share the parsed data, a response body can be read only once, + // and a failed fetch must not stay cached for the next calls + const runFetch = () => { + let fetching = runtimeFetchings[key]; + if (!fetching) { + fetching = doFetch().finally(() => { + delete runtimeFetchings[key]; + }); + runtimeFetchings[key] = fetching; + } + return fetching; + }; const docSnap = await docRef.get(); if (docSnap.exists) { const cacheVal = docSnap.data() as CacheValue;