@reddoorla/maintenance 0.54.4 → 0.55.0
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.
- package/dist/cli/bin.js +101 -5
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +6 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -5
package/dist/cli/bin.js
CHANGED
|
@@ -174,6 +174,7 @@ __export(websites_exports, {
|
|
|
174
174
|
parseSecurityAdvisories: () => parseSecurityAdvisories,
|
|
175
175
|
siteSlug: () => siteSlug,
|
|
176
176
|
updateA11yCounts: () => updateA11yCounts,
|
|
177
|
+
updateAnalyticsHealth: () => updateAnalyticsHealth,
|
|
177
178
|
updateAuditFields: () => updateAuditFields,
|
|
178
179
|
updateDepsCounts: () => updateDepsCounts,
|
|
179
180
|
updateGitHubSignals: () => updateGitHubSignals,
|
|
@@ -234,6 +235,7 @@ function mapRow(rec) {
|
|
|
234
235
|
ga4PropertyId: f["GA4 property ID"] ?? null,
|
|
235
236
|
searchQuery: f["Search query"] ?? null,
|
|
236
237
|
searchConsoleProperty: f["Search Console property"] ?? null,
|
|
238
|
+
analyticsSoftFailAt: f["Analytics soft-fail at"] ?? null,
|
|
237
239
|
gitRepo: f["Git repo"] ?? null,
|
|
238
240
|
reportRecipientsTo: f["Report recipients (To)"] ?? null,
|
|
239
241
|
reportRecipientsCc: f["Report recipients (CC)"] ?? null,
|
|
@@ -376,6 +378,10 @@ function browserFields(r) {
|
|
|
376
378
|
async function updateScores(base, recordId, scores) {
|
|
377
379
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);
|
|
378
380
|
}
|
|
381
|
+
async function updateAnalyticsHealth(base, recordId, at) {
|
|
382
|
+
const fields = { "Analytics soft-fail at": at };
|
|
383
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
384
|
+
}
|
|
379
385
|
async function updateA11yCounts(base, recordId, counts) {
|
|
380
386
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);
|
|
381
387
|
}
|
|
@@ -1629,6 +1635,10 @@ var init_attachments = __esm({
|
|
|
1629
1635
|
});
|
|
1630
1636
|
|
|
1631
1637
|
// src/reports/send/resend.ts
|
|
1638
|
+
var resend_exports = {};
|
|
1639
|
+
__export(resend_exports, {
|
|
1640
|
+
defaultResendClient: () => defaultResendClient
|
|
1641
|
+
});
|
|
1632
1642
|
import { Resend } from "resend";
|
|
1633
1643
|
function defaultResendClient() {
|
|
1634
1644
|
const key = process.env.RESEND_API_KEY;
|
|
@@ -1777,7 +1787,26 @@ function collectCiAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
|
1777
1787
|
}
|
|
1778
1788
|
return items;
|
|
1779
1789
|
}
|
|
1780
|
-
|
|
1790
|
+
function collectAnalyticsFailures(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
1791
|
+
const items = [];
|
|
1792
|
+
for (const s of sites) {
|
|
1793
|
+
const at = s.analyticsSoftFailAt;
|
|
1794
|
+
if (at === null) continue;
|
|
1795
|
+
const ageMs = now.getTime() - Date.parse(at);
|
|
1796
|
+
if (Number.isFinite(ageMs) && ageMs > ANALYTICS_SOFT_FAIL_STALE_DAYS * MS_PER_DAY4) continue;
|
|
1797
|
+
items.push({
|
|
1798
|
+
key: `analytics:${s.id}`,
|
|
1799
|
+
kind: "analytics",
|
|
1800
|
+
siteName: s.name,
|
|
1801
|
+
title: "GA/Search enrichment failing (analytics blank)",
|
|
1802
|
+
url: dashboardUrl(baseUrl, s.name),
|
|
1803
|
+
severity: "warning",
|
|
1804
|
+
metric: 1
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
return items;
|
|
1808
|
+
}
|
|
1809
|
+
var GITHUB_SIGNALS_STALE_DAYS, MS_PER_DAY4, LIGHTHOUSE_FLOOR, LIGHTHOUSE_CATEGORIES2, ANALYTICS_SOFT_FAIL_STALE_DAYS;
|
|
1781
1810
|
var init_digest_collectors = __esm({
|
|
1782
1811
|
"src/alerts/digest-collectors.ts"() {
|
|
1783
1812
|
"use strict";
|
|
@@ -1791,6 +1820,7 @@ var init_digest_collectors = __esm({
|
|
|
1791
1820
|
{ field: "bpScore", slug: "best-practices", label: "Best Practices" },
|
|
1792
1821
|
{ field: "seoScore", slug: "seo", label: "SEO" }
|
|
1793
1822
|
];
|
|
1823
|
+
ANALYTICS_SOFT_FAIL_STALE_DAYS = 45;
|
|
1794
1824
|
}
|
|
1795
1825
|
});
|
|
1796
1826
|
|
|
@@ -1946,7 +1976,8 @@ async function collectAttention(deps) {
|
|
|
1946
1976
|
...runCollector("delivery", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),
|
|
1947
1977
|
...runCollector("lighthouse", () => collectLighthouseAlerts(websites, deps.baseUrl)),
|
|
1948
1978
|
...runCollector("renovate", () => collectRenovateAlerts(websites, deps.baseUrl, now)),
|
|
1949
|
-
...runCollector("ci", () => collectCiAlerts(websites, deps.baseUrl, now))
|
|
1979
|
+
...runCollector("ci", () => collectCiAlerts(websites, deps.baseUrl, now)),
|
|
1980
|
+
...runCollector("analytics", () => collectAnalyticsFailures(websites, deps.baseUrl, now))
|
|
1950
1981
|
];
|
|
1951
1982
|
}
|
|
1952
1983
|
async function runDigest(options) {
|
|
@@ -6817,6 +6848,17 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
6817
6848
|
return { reportRow: null, htmlPath: path, html, softFailures, queued: null, supersededIds: [] };
|
|
6818
6849
|
}
|
|
6819
6850
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
6851
|
+
if (readGaConfig() !== null && Boolean(siteRow.ga4PropertyId || siteRow.searchQuery)) {
|
|
6852
|
+
try {
|
|
6853
|
+
await updateAnalyticsHealth(
|
|
6854
|
+
base,
|
|
6855
|
+
siteRow.id,
|
|
6856
|
+
softFailures.length > 0 ? today.toISOString() : null
|
|
6857
|
+
);
|
|
6858
|
+
} catch (e) {
|
|
6859
|
+
console.warn(`\u26A0 analytics-health write skipped for ${siteRow.name}: ${e.message}`);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6820
6862
|
if (options.completeRowId) {
|
|
6821
6863
|
await uploadDraftHtml(options.completeRowId, slug, periodEnd, html);
|
|
6822
6864
|
const outcome2 = await queueDraft(base, {
|
|
@@ -6919,6 +6961,25 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
|
6919
6961
|
return start;
|
|
6920
6962
|
}
|
|
6921
6963
|
|
|
6964
|
+
// src/alerts/analytics-health.ts
|
|
6965
|
+
init_html();
|
|
6966
|
+
var MIN_FAILED_SITES = 2;
|
|
6967
|
+
function assessAnalyticsAlert(h) {
|
|
6968
|
+
const { softFailedSites, configuredSites } = h;
|
|
6969
|
+
const fire = configuredSites >= 2 && softFailedSites >= MIN_FAILED_SITES && softFailedSites * 2 >= configuredSites;
|
|
6970
|
+
const reason = fire ? `${softFailedSites} of ${configuredSites} analytics-configured sites had GA/Search enrichment fail this run \u2014 the shared GA_SUBJECT likely lost access (an offboarded user, revoked property access, or a botched role-account cutover). Reports were drafted with BLANK analytics.` : "";
|
|
6971
|
+
return { fire, reason };
|
|
6972
|
+
}
|
|
6973
|
+
function composeAnalyticsAlertEmail(h, dashboardUrl2) {
|
|
6974
|
+
const subject = `\u26A0 Fleet analytics enrichment failing \u2014 ${h.softFailedSites}/${h.configuredSites} sites`;
|
|
6975
|
+
const { reason } = assessAnalyticsAlert(h);
|
|
6976
|
+
const html = `<p><strong>${escapeHtml(reason)}</strong></p>
|
|
6977
|
+
<p>This usually means the Google Workspace user the service account impersonates can no longer read the GA4 / Search Console properties. Reports still send \u2014 but with blank analytics \u2014 until the subject is restored.</p>
|
|
6978
|
+
<p>Next step: follow the GA/Search subject runbook (<code>docs/runbooks/ga-search-role-account-cutover.md</code>) to restore or move the subject, then re-run <code>reddoor-maint report --due</code> and confirm the warning clears.</p>
|
|
6979
|
+
<p><a href="${escapeHtml(dashboardUrl2)}">Open the fleet dashboard \u2192</a></p>`;
|
|
6980
|
+
return { subject, html };
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6922
6983
|
// src/cli/commands/report.ts
|
|
6923
6984
|
function draftLine(reportId, queued, supersededIds, verb = "drafted") {
|
|
6924
6985
|
const id = reportId ?? "(unknown)";
|
|
@@ -6969,15 +7030,44 @@ async function runReportCommand(slug, opts) {
|
|
|
6969
7030
|
}
|
|
6970
7031
|
async function runDueDraft() {
|
|
6971
7032
|
const base = openBase(readAirtableConfig());
|
|
6972
|
-
|
|
7033
|
+
const result = await draftDueReports(base, /* @__PURE__ */ new Date());
|
|
7034
|
+
await alertOnFleetAnalyticsFailure(result.health);
|
|
7035
|
+
return { output: result.output, code: result.code };
|
|
7036
|
+
}
|
|
7037
|
+
async function alertOnFleetAnalyticsFailure(health) {
|
|
7038
|
+
if (!assessAnalyticsAlert(health).fire) return;
|
|
7039
|
+
try {
|
|
7040
|
+
const to = process.env.OPERATOR_EMAIL?.trim() || "info@reddoorla.com";
|
|
7041
|
+
const { subject, html } = composeAnalyticsAlertEmail(health, dashboardBaseUrl());
|
|
7042
|
+
const { defaultResendClient: defaultResendClient2 } = await Promise.resolve().then(() => (init_resend(), resend_exports));
|
|
7043
|
+
await defaultResendClient2().send({
|
|
7044
|
+
from: "Reddoor Reports <reports@reddoorla.com>",
|
|
7045
|
+
to: [to],
|
|
7046
|
+
subject,
|
|
7047
|
+
html,
|
|
7048
|
+
idempotencyKey: `analytics-alert-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
7049
|
+
});
|
|
7050
|
+
console.warn(`\u26A0 ${subject} \u2014 operator alerted (${to})`);
|
|
7051
|
+
} catch (e) {
|
|
7052
|
+
console.warn(`\u26A0 analytics-failure alert send failed: ${e.message}`);
|
|
7053
|
+
}
|
|
6973
7054
|
}
|
|
6974
7055
|
async function draftDueReports(base, today) {
|
|
6975
7056
|
const websites = await listWebsites(base);
|
|
6976
7057
|
const reports = await listAllReports(base);
|
|
6977
7058
|
const due = findDueReports(websites, reports, today);
|
|
6978
|
-
|
|
7059
|
+
const gaConfigured = readGaConfig() !== null;
|
|
7060
|
+
const isAnalyticsConfigured = (s) => gaConfigured && Boolean(s.ga4PropertyId || s.searchQuery);
|
|
7061
|
+
if (due.length === 0) {
|
|
7062
|
+
return {
|
|
7063
|
+
output: "No reports due.",
|
|
7064
|
+
code: 0,
|
|
7065
|
+
health: { softFailedSites: 0, configuredSites: 0 }
|
|
7066
|
+
};
|
|
7067
|
+
}
|
|
6979
7068
|
const lines = [];
|
|
6980
7069
|
let softFailedSites = 0;
|
|
7070
|
+
let gaConfiguredSites = 0;
|
|
6981
7071
|
let skipped = 0;
|
|
6982
7072
|
for (const item of due) {
|
|
6983
7073
|
const period = reportPeriodKey(item.dueDate);
|
|
@@ -7015,6 +7105,7 @@ async function draftDueReports(base, today) {
|
|
|
7015
7105
|
"completed half-made draft"
|
|
7016
7106
|
)
|
|
7017
7107
|
);
|
|
7108
|
+
if (isAnalyticsConfigured(item.site)) gaConfiguredSites++;
|
|
7018
7109
|
if (result.softFailures.length > 0) softFailedSites++;
|
|
7019
7110
|
} catch (e) {
|
|
7020
7111
|
lines.push(`\u2717 failed: ${item.site.name} ${item.reportType} \u2014 ${e.message}`);
|
|
@@ -7035,6 +7126,7 @@ async function draftDueReports(base, today) {
|
|
|
7035
7126
|
const result = await draftReportForSite(base, item.site, item.reportType, { period });
|
|
7036
7127
|
lines.push(draftLine(result.reportRow?.reportId, result.queued, result.supersededIds));
|
|
7037
7128
|
if (result.reportRow) reports.push(result.reportRow);
|
|
7129
|
+
if (isAnalyticsConfigured(item.site)) gaConfiguredSites++;
|
|
7038
7130
|
if (result.softFailures.length > 0) softFailedSites++;
|
|
7039
7131
|
} catch (e) {
|
|
7040
7132
|
lines.push(`\u2717 failed: ${item.site.name} ${item.reportType} \u2014 ${e.message}`);
|
|
@@ -7048,7 +7140,11 @@ async function draftDueReports(base, today) {
|
|
|
7048
7140
|
`\u26A0 ${softFailedSites} site${softFailedSites === 1 ? "" : "s"} had GA/Search enrichment fail \u2014 drafted with blank analytics; check the logs above`
|
|
7049
7141
|
);
|
|
7050
7142
|
}
|
|
7051
|
-
return {
|
|
7143
|
+
return {
|
|
7144
|
+
output: lines.join("\n"),
|
|
7145
|
+
code: lines.some((l) => l.startsWith("\u2717")) ? 1 : 0,
|
|
7146
|
+
health: { softFailedSites, configuredSites: gaConfiguredSites }
|
|
7147
|
+
};
|
|
7052
7148
|
}
|
|
7053
7149
|
async function runSingleSiteDraft(slug, opts) {
|
|
7054
7150
|
const base = openBase(readAirtableConfig());
|