Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | 104x 104x 2243x 70x 56x 56x 56x 56x 56x 64x 64x 55x 1x 104x 1x 2225x 104x 104x 26x 104x 11x 11x 11x 3x 11x 165x 4795x 5457x 2049x 4795x 2049x 1588x 1588x 104x 19x 34x 34x 3x 104x 104x 111x 107x 106x 110x 20x 20x 3x 171x 9x 9x 3x | import { buildUrlFromParams, ParamTypes, parseLocationParams } from "../url-params.js";
import { parameterFor, Parameters, urlParamType } from "./parameters.js";
import { errorLabel } from "./analytics-filters.js";
const MaxErrorsNoted = 10;
const StackFrame = /(?:^\s*at |@).*?([^\s(@]+):(\d+):(\d+)\)?\s*$/;
const measurementId = () => import.meta.env.GA_MEASUREMENT_ID;
/**
* `href` with each parameter put through its `analyticsFilter`, read and written as the page reads and writes its own
* URL; with no parameters at all when they cannot be decoded.
*/
export function analyticsLocation(href) {
if (!href) return href;
const url = new URL(href);
const page = url.origin + url.pathname;
try {
const params = parseLocationParams(url, urlParamType);
for (const [key, value] of Object.entries(params)) {
const filter = parameterFor(key)?.parameter.analyticsFilter;
if (filter && value !== null) params[key] = Array.isArray(value) ? value.map(filter) : filter(value);
}
return buildUrlFromParams(page, params, urlParamType);
} catch {
return page;
}
}
let silenced = false;
/** Sends nothing from here on: benchmark runs, many of them scripted, would swamp the counts. */
export function silenceAnalytics() {
silenced = true;
}
/*global gtag*/
const injectedGtag = () => (!silenced && measurementId() && typeof gtag === "function" ? gtag : undefined);
const InstalledDisplayModes = ["standalone", "window-controls-overlay", "minimal-ui", "fullscreen"];
/** How the page is shown: the installed app's display mode, or "browser" for a tab. */
export const displayMode = () =>
InstalledDisplayModes.find((mode) => window.matchMedia(`(display-mode: ${mode})`).matches) ??
// Safari on iOS has no display-mode query for a page added to the home screen.
(window.navigator.standalone ? "standalone" : "browser");
const pageLocations = () => ({
page_location: analyticsLocation(window.location.href),
page_referrer: analyticsLocation(document.referrer),
});
/**
* Sends the page_view, telling Google Analytics where the page is as analyticsLocation does and how it is shown, and
* notes the errors `page` reports and its being installed as an app.
*/
export function configureAnalytics(page = window) {
injectedGtag()?.("config", measurementId(), { ...pageLocations(), display_mode: displayMode() });
page.addEventListener("appinstalled", () => note("app_installed", {}));
page.addEventListener("error", (event) =>
noteError("window", event.error ?? event.message, {
file: event.filename,
line: event.lineno,
column: event.colno,
}),
);
page.addEventListener("unhandledrejection", (event) => noteError("promise", event.reason));
}
/** Moves the location later events report to the URL the page has just pushed, without a page_view. */
export function noteLocation() {
injectedGtag()?.("config", measurementId(), { ...pageLocations(), send_page_view: false });
}
const logValue = (value) => (typeof value === "string" && /^$|[\s"=]/.test(value) ? JSON.stringify(value) : value);
/**
* Sends `event`, leaving out the parameters that are undefined, and logs what it sends, or would send, as one line of
* text.
*/
export function note(event, params) {
const sent = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));
injectedGtag()?.("event", event, sent);
const pairs = Object.entries(sent).map(([key, value]) => ` ${key}=${logValue(value)}`);
console.log(`event noted: ${event}${pairs.join("")}`);
}
/**
* Notes `action` on a feature: the `data-feature` that `feature`, an element, is or is inside, or nothing without
* one; or `feature` itself, for a feature with no element to declare it on.
*/
export function noteFeature(feature, action) {
const name = typeof feature === "string" ? feature : feature?.closest("[data-feature]")?.dataset.feature;
if (name) note("feature_used", { feature: name, action });
}
const fileName = (url) => url.split(/[?#]/)[0].split("/").pop();
function stackPlace(stack) {
for (const line of String(stack ?? "").split("\n")) {
const frame = line.match(StackFrame);
if (frame) return `${fileName(frame[1])}:${frame[2]}:${frame[3]}`;
}
return undefined;
}
const errorsNoted = new WeakSet();
let errorCount = 0;
/** Notes an error once however many places see it (the loop rethrows what it notes), and only the first few. */
export function noteError(where, error, { file, line, column } = {}) {
if (Object(error) === error) {
if (errorsNoted.has(error)) return;
errorsNoted.add(error);
}
if (errorCount >= MaxErrorsNoted) return;
++errorCount;
note("js_error", {
where,
message: errorLabel(error?.message ?? error),
file_line: file ? `${fileName(file)}:${line}:${column}` : stackPlace(error?.stack),
});
}
/**
* Notes how the machine started: every parameter declared `reportedAtStart` as it is now, and what the page `found`,
* once `audio` resolves to how the sound came up.
*/
export async function noteMachineStart(settings, params, found, audio) {
const reported = Object.entries(Parameters)
.filter(([, parameter]) => parameter.reportedAtStart)
.map(([name, { type }]) => {
const value = settings.value(name) ?? params[name];
return [name, type === ParamTypes.BOOL ? !!value : value];
});
note("machine_start", { ...Object.fromEntries(reported), ...found, audio: await audio });
}
|