文件
hyb-oa/.trellis/tasks/archive/2026-08/08-11-internal-oa-usage-census-and-simplification/research/usage-census.cjs
T

431 行
20 KiB
JavaScript

"use strict";
const path = require("node:path");
const MODULE_KEYS = Object.freeze([
"portal_notifications",
"workflow_approvals",
"reimbursements_invoices",
"payments_ap",
"procurement",
"contracts",
"projects",
"assets_inventory",
"leave_wecom_history",
"attendance_presence",
"weekly_reports",
"todos",
"announcements",
"budgets",
"travel",
"people_lifecycle",
"payroll",
"performance",
"system_configuration",
"integrations",
]);
const SAFE_RESULT_KEYS = Object.freeze([
"moduleKey",
"totalRows",
"created30d",
"created90d",
"activity30d",
"activity90d",
"latestActivityAt",
"humanActors30d",
"humanActors90d",
"humanEvents30d",
"humanEvents90d",
"smokeEvents30d",
"smokeEvents90d",
"systemEvents30d",
"systemEvents90d",
"unknownEvents30d",
"unknownEvents90d",
"auditEvents30d",
"auditEvents90d",
"auditHumanActors30d",
"auditHumanActors90d",
"auditHumanEvents30d",
"auditHumanEvents90d",
"auditSmokeEvents30d",
"auditSmokeEvents90d",
"auditUnknownEvents30d",
"auditUnknownEvents90d",
"navigationMode",
"effectiveFlagState",
"queryStatus",
]);
const USAGE_SQL = String.raw`
WITH module_keys(module_key) AS (
VALUES
('portal_notifications'), ('workflow_approvals'),
('reimbursements_invoices'), ('payments_ap'), ('procurement'),
('contracts'), ('projects'), ('assets_inventory'),
('leave_wecom_history'), ('attendance_presence'), ('weekly_reports'),
('todos'), ('announcements'), ('budgets'), ('travel'),
('people_lifecycle'), ('payroll'), ('performance'),
('system_configuration'), ('integrations')
), record_events(module_key, record_id, created_at, activity_at, actor_id, forced_kind) AS (
SELECT 'portal_notifications', id::text, created_at, created_at, recipient_employee_id, 'system' FROM notifications
UNION ALL SELECT 'workflow_approvals', id::text, created_at, updated_at, submitter_id, NULL FROM workflow_instances
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, updated_at, owner_id, NULL FROM reimbursement_forms
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, updated_at, uploaded_by_employee_id, NULL FROM invoice_pool
UNION ALL SELECT 'reimbursements_invoices', id::text, created_at, created_at, NULL::uuid, 'system' FROM ocr_results
UNION ALL SELECT 'payments_ap', id::text, created_at, updated_at, created_by, NULL FROM payment_batches
UNION ALL SELECT 'payments_ap', id::text, created_at, created_at, uploaded_by, NULL FROM payment_batch_receipt_attachments
UNION ALL SELECT 'procurement', id::text, created_at, updated_at, applicant_id, NULL FROM purchase_requests
UNION ALL SELECT 'contracts', id::text, created_at, updated_at, owner_employee_id, NULL FROM contract_registry
UNION ALL SELECT 'projects', id::text, created_at, updated_at, owner_employee_id, NULL FROM projects
UNION ALL SELECT 'assets_inventory', id::text, created_at, updated_at, created_by, NULL FROM asset_cards
UNION ALL SELECT 'assets_inventory', id::text, created_at, updated_at, created_by, NULL FROM inventory_checks
UNION ALL SELECT 'leave_wecom_history', id::text, created_at, updated_at, owner_id, NULL FROM leave_requests
UNION ALL SELECT 'leave_wecom_history', id::text, created_at, updated_at, applicant_employee_id, 'system' FROM wecom_approval_archives
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, owner_id, NULL FROM attendance_corrections
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, employee_id, 'system' FROM wecom_checkin_records
UNION ALL SELECT 'attendance_presence', id::text, created_at, updated_at, updated_by, NULL FROM employee_presence
UNION ALL SELECT 'weekly_reports', id::text, created_at, updated_at, employee_id, NULL FROM weekly_reports
UNION ALL SELECT 'todos', id::text, created_at, updated_at, created_by, NULL FROM todo_items
UNION ALL SELECT 'announcements', id::text, created_at, updated_at, created_by, NULL FROM portal_announcements
UNION ALL SELECT 'announcements', id::text, created_at, COALESCE(acknowledged_at, created_at), actor_id, NULL FROM portal_announcement_acknowledgements
UNION ALL SELECT 'budgets', id::text, created_at, updated_at, created_by, NULL FROM budget_pools
UNION ALL SELECT 'travel', id::text, created_at, updated_at, applicant_employee_id, NULL FROM travel_requests
UNION ALL SELECT 'travel', id::text, created_at, updated_at, created_by, NULL FROM travel_orders
UNION ALL SELECT 'people_lifecycle', id::text, created_at, created_at, created_by, NULL FROM employee_lifecycle_events
UNION ALL SELECT 'people_lifecycle', id::text, created_at, updated_at, created_by, NULL FROM employee_contracts
UNION ALL SELECT 'people_lifecycle', id::text, created_at, updated_at, created_by, NULL FROM employee_trainings
UNION ALL SELECT 'payroll', id::text, created_at, updated_at, created_by, NULL FROM payslips
UNION ALL SELECT 'performance', id::text, created_at, updated_at, reviewer_employee_id, NULL FROM performance_reviews
UNION ALL SELECT 'system_configuration', id::text, created_at, updated_at, updated_by, NULL FROM feature_flags
UNION ALL SELECT 'system_configuration', id::text, created_at, updated_at, updated_by, NULL FROM cutover_scopes
UNION ALL SELECT 'integrations', id::text, created_at, updated_at, created_by, 'system' FROM integration_outbox
), classified_records AS (
SELECT
r.*,
CASE
WHEN r.forced_kind = 'system' THEN 'system'
WHEN r.actor_id IS NULL OR e.id IS NULL THEN 'unknown'
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
OR e.name ~ '(测试|试运行)'
THEN 'smoke'
ELSE 'human'
END AS actor_kind
FROM record_events r
LEFT JOIN employees e ON e.id = r.actor_id
), audit_mapped AS (
SELECT
CASE
WHEN action ~ '^(APPROVAL_|DELEGATION_|WORKFLOW_)' THEN 'workflow_approvals'
WHEN action ~ '^(REIMBURSEMENT_|INVOICE_)' OR action = 'EXPORT_REIMBURSEMENTS' THEN 'reimbursements_invoices'
WHEN action ~ '^PAYMENT_' OR action = 'EXPORT_AP_OPERATIONS' THEN 'payments_ap'
WHEN action ~ '^PURCHASE_' THEN 'procurement'
WHEN action ~ '^CONTRACT_' OR action = 'EXPORT_CONTRACTS_REGISTRY' THEN 'contracts'
WHEN action ~ '^PROJECT_' THEN 'projects'
WHEN action ~ '^(ASSET_|INVENTORY_)' THEN 'assets_inventory'
WHEN action ~ '^(ATTENDANCE_|PRESENCE_)' THEN 'attendance_presence'
WHEN action ~ '^PORTAL_ANNOUNCEMENT_' THEN 'announcements'
WHEN action ~ '^BUDGET_' THEN 'budgets'
WHEN action ~ '^TRAVEL_' THEN 'travel'
WHEN action ~ '^(EMPLOYEE_|SYSTEM_USER|DEPARTMENT_)' THEN 'people_lifecycle'
WHEN action ~ '^(CREATE_SALARY_|UPDATE_SALARY_|UPSERT_EMPLOYEE_SALARY_|GENERATE_PAYSLIPS|ISSUE_PAYSLIPS|UPDATE_PAYSLIP|EXPORT_PAYSLIPS)' THEN 'payroll'
WHEN action ~ '^PERFORMANCE_' THEN 'performance'
WHEN action ~ '^(FEATURE_FLAG_|WORKSPACE_)' THEN 'system_configuration'
WHEN action ~ '^ERP_' THEN 'integrations'
ELSE NULL
END AS module_key,
a.actor_id,
a.created_at,
CASE
WHEN e.id IS NULL THEN 'unknown'
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
OR e.name ~ '(测试|试运行)'
THEN 'smoke'
ELSE 'human'
END AS actor_kind
FROM audit_logs a
LEFT JOIN employees e ON e.id = a.actor_id
), record_metrics AS (
SELECT
module_key,
count(*)::int AS total_rows,
count(*) FILTER (WHERE created_at >= now() - interval '30 days')::int AS created_30d,
count(*) FILTER (WHERE created_at >= now() - interval '90 days')::int AS created_90d,
count(*) FILTER (WHERE activity_at >= now() - interval '30 days')::int AS activity_30d,
count(*) FILTER (WHERE activity_at >= now() - interval '90 days')::int AS activity_90d,
max(activity_at) AS latest_activity_at,
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '30 days')::int AS human_actors_30d,
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '90 days')::int AS human_actors_90d,
count(*) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '30 days')::int AS human_events_30d,
count(*) FILTER (WHERE actor_kind = 'human' AND activity_at >= now() - interval '90 days')::int AS human_events_90d,
count(*) FILTER (WHERE actor_kind = 'smoke' AND activity_at >= now() - interval '30 days')::int AS smoke_events_30d,
count(*) FILTER (WHERE actor_kind = 'smoke' AND activity_at >= now() - interval '90 days')::int AS smoke_events_90d,
count(*) FILTER (WHERE actor_kind = 'system' AND activity_at >= now() - interval '30 days')::int AS system_events_30d,
count(*) FILTER (WHERE actor_kind = 'system' AND activity_at >= now() - interval '90 days')::int AS system_events_90d,
count(*) FILTER (WHERE actor_kind = 'unknown' AND activity_at >= now() - interval '30 days')::int AS unknown_events_30d,
count(*) FILTER (WHERE actor_kind = 'unknown' AND activity_at >= now() - interval '90 days')::int AS unknown_events_90d
FROM classified_records
GROUP BY module_key
), audit_metrics AS (
SELECT
module_key,
count(*) FILTER (WHERE created_at >= now() - interval '30 days')::int AS audit_events_30d,
count(*) FILTER (WHERE created_at >= now() - interval '90 days')::int AS audit_events_90d,
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS audit_human_actors_30d,
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS audit_human_actors_90d,
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS audit_human_events_30d,
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS audit_human_events_90d,
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '30 days')::int AS audit_smoke_events_30d,
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '90 days')::int AS audit_smoke_events_90d,
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '30 days')::int AS audit_unknown_events_30d,
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '90 days')::int AS audit_unknown_events_90d
FROM audit_mapped
WHERE module_key IS NOT NULL
GROUP BY module_key
)
SELECT
k.module_key AS "moduleKey",
COALESCE(r.total_rows, 0) AS "totalRows",
COALESCE(r.created_30d, 0) AS "created30d",
COALESCE(r.created_90d, 0) AS "created90d",
COALESCE(r.activity_30d, 0) AS "activity30d",
COALESCE(r.activity_90d, 0) AS "activity90d",
r.latest_activity_at AS "latestActivityAt",
COALESCE(r.human_actors_30d, 0) AS "humanActors30d",
COALESCE(r.human_actors_90d, 0) AS "humanActors90d",
COALESCE(r.human_events_30d, 0) AS "humanEvents30d",
COALESCE(r.human_events_90d, 0) AS "humanEvents90d",
COALESCE(r.smoke_events_30d, 0) AS "smokeEvents30d",
COALESCE(r.smoke_events_90d, 0) AS "smokeEvents90d",
COALESCE(r.system_events_30d, 0) AS "systemEvents30d",
COALESCE(r.system_events_90d, 0) AS "systemEvents90d",
COALESCE(r.unknown_events_30d, 0) AS "unknownEvents30d",
COALESCE(r.unknown_events_90d, 0) AS "unknownEvents90d",
COALESCE(a.audit_events_30d, 0) AS "auditEvents30d",
COALESCE(a.audit_events_90d, 0) AS "auditEvents90d",
COALESCE(a.audit_human_actors_30d, 0) AS "auditHumanActors30d",
COALESCE(a.audit_human_actors_90d, 0) AS "auditHumanActors90d",
COALESCE(a.audit_human_events_30d, 0) AS "auditHumanEvents30d",
COALESCE(a.audit_human_events_90d, 0) AS "auditHumanEvents90d",
COALESCE(a.audit_smoke_events_30d, 0) AS "auditSmokeEvents30d",
COALESCE(a.audit_smoke_events_90d, 0) AS "auditSmokeEvents90d",
COALESCE(a.audit_unknown_events_30d, 0) AS "auditUnknownEvents30d",
COALESCE(a.audit_unknown_events_90d, 0) AS "auditUnknownEvents90d"
FROM module_keys k
LEFT JOIN record_metrics r ON r.module_key = k.module_key
LEFT JOIN audit_metrics a ON a.module_key = k.module_key
ORDER BY k.module_key
`;
const AUTH_SQL = String.raw`
WITH classified AS (
SELECT
a.actor_id,
a.created_at,
CASE
WHEN e.id IS NULL THEN 'unknown'
WHEN lower(split_part(e.email, '@', 1)) ~ '^(smoke[-_.]|oa[-_.]?smoke[-_.]?)'
OR e.employee_no ~* '^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)'
OR e.name ~ '(测试|试运行)'
THEN 'smoke'
ELSE 'human'
END AS actor_kind
FROM audit_logs a
LEFT JOIN employees e ON e.id = a.actor_id
WHERE a.action IN ('AUTH_LOGIN_SUCCESS', 'AUTH_EMAIL_LOGIN_SUCCESS', 'AUTH_OIDC_LOGIN_SUCCESS')
)
SELECT
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS "humanActors30d",
count(DISTINCT actor_id) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS "humanActors90d",
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '30 days')::int AS "humanEvents30d",
count(*) FILTER (WHERE actor_kind = 'human' AND created_at >= now() - interval '90 days')::int AS "humanEvents90d",
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '30 days')::int AS "smokeEvents30d",
count(*) FILTER (WHERE actor_kind = 'smoke' AND created_at >= now() - interval '90 days')::int AS "smokeEvents90d",
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '30 days')::int AS "unknownEvents30d",
count(*) FILTER (WHERE actor_kind = 'unknown' AND created_at >= now() - interval '90 days')::int AS "unknownEvents90d"
FROM classified
`;
function isSmokeLikeValue(value) {
const text = String(value ?? "").trim();
if (!text) return false;
const lower = text.toLowerCase();
const emailLocal = lower.includes("@") ? lower.split("@")[0] : lower;
return (
/^(smoke[-_.]|oa[-_.]?smoke[-_.]?)/u.test(emailLocal)
|| /^(SMOKE|OSMOKE|WFULL|PRELAUNCH|LAUNCHAUTO|HRWDTEST)[A-Z0-9_-]*/iu.test(text)
|| /测试|试运行/u.test(text)
);
}
function classifyFixtureActor(actor, forcedKind = null) {
if (forcedKind === "system") return "system";
if (!actor) return "unknown";
if ([actor.employeeNo, actor.email, actor.name].some(isSmokeLikeValue)) return "smoke";
return "human";
}
function createReadOnlyAdapter(client) {
return {
featureFlag: {
async findUnique({ where }) {
const result = await client.query(
"SELECT flag_key, override_mode, rollout_type, department_ids, note, version, created_at, created_by, updated_at, updated_by FROM feature_flags WHERE flag_key = $1",
[where.flag_key],
);
return result.rows[0] ?? null;
},
async findMany({ where }) {
const result = await client.query(
"SELECT flag_key, override_mode, rollout_type, department_ids, note, version, created_at, created_by, updated_at, updated_by FROM feature_flags WHERE flag_key = ANY($1::text[]) ORDER BY flag_key ASC",
[where.flag_key.in],
);
return result.rows;
},
},
workspaceSetting: {
async findFirst() {
const result = await client.query(
"SELECT profile, org_mode, primary_entry_mode, notification_strategy, device_priority, module_visibility_json, created_at, updated_at, updated_by FROM workspace_settings ORDER BY created_at ASC LIMIT 1",
);
return result.rows[0] ?? null;
},
},
};
}
function effectiveFlagState(item) {
if (item.requiresDepartment && item.effectiveReason === "department_deferred") return "partial";
return item.effectiveEnabled ? "on" : "off";
}
function applyExposure(metrics, workspace, flags) {
const flagState = new Map(flags.map((item) => [item.flagKey, effectiveFlagState(item)]));
const moduleExposure = {
portal_notifications: ["portal", "portal_home_v1"],
workflow_approvals: ["workflowTasks", null],
reimbursements_invoices: ["reimbursements", null],
payments_ap: ["paymentBatches", "payment_batch_v1"],
procurement: ["purchaseRequests", null],
contracts: ["contractsRegistry", "project_linking_v1"],
projects: ["projects", "project_domain_v1"],
assets_inventory: ["assets", null],
leave_wecom_history: ["leaveRequests", null],
attendance_presence: ["attendance", "attendance_unified_v1"],
weekly_reports: ["weeklyReports", "weekly_report_v1"],
todos: ["todos", "todo_board_v1"],
announcements: ["announcements", "announcements_v1"],
budgets: ["budgets", null],
travel: ["travelRequests", null],
people_lifecycle: ["people", "employee_lifecycle_v1"],
payroll: [null, null],
performance: [null, null],
system_configuration: ["featureFlags", null],
integrations: [null, "erp_sync"],
};
return metrics.map((metric) => {
const [navigationKey, flagKey] = moduleExposure[metric.moduleKey] ?? [null, null];
const navigationMode = navigationKey
? workspace.moduleVisibility[navigationKey] ?? "unknown"
: "no_navigation";
const effectiveState = flagKey ? flagState.get(flagKey) ?? "unknown" : "not_applicable";
return {
...metric,
latestActivityAt: metric.latestActivityAt?.toISOString?.() ?? metric.latestActivityAt ?? null,
navigationMode,
effectiveFlagState: effectiveState,
queryStatus: "PASS",
};
});
}
function assertSafeResult(metrics) {
if (!Array.isArray(metrics) || metrics.length !== MODULE_KEYS.length) {
throw new Error("result_module_count_invalid");
}
const seen = new Set();
for (const item of metrics) {
if (!MODULE_KEYS.includes(item.moduleKey) || seen.has(item.moduleKey)) {
throw new Error("result_module_key_invalid");
}
seen.add(item.moduleKey);
const keys = Object.keys(item).sort();
const expected = [...SAFE_RESULT_KEYS].sort();
if (JSON.stringify(keys) !== JSON.stringify(expected)) {
throw new Error("result_field_contract_invalid");
}
}
}
async function run() {
const appRoot = path.resolve(process.env.USAGE_CENSUS_APP_ROOT || process.cwd());
const { Client } = require(path.join(appRoot, "node_modules/pg"));
const { getConfig } = require(path.join(appRoot, "src/config.js"));
const { listFeatureFlags } = require(path.join(appRoot, "src/services/featureFlags/index.js"));
const { getWorkspaceSetting } = require(path.join(appRoot, "src/services/workspaceSettings.js"));
const config = getConfig();
const client = new Client({ connectionString: process.env.DATABASE_URL });
let inTransaction = false;
try {
await client.connect();
await client.query("BEGIN TRANSACTION READ ONLY");
inTransaction = true;
await client.query("SET LOCAL statement_timeout = '30s'");
await client.query("SET LOCAL lock_timeout = '2s'");
const readOnly = await client.query("SELECT current_setting('transaction_read_only') AS value");
if (readOnly.rows[0]?.value !== "on") throw new Error("transaction_not_read_only");
const adapter = createReadOnlyAdapter(client);
const [metricResult, authResult, flags, workspace] = await Promise.all([
client.query(USAGE_SQL),
client.query(AUTH_SQL),
listFeatureFlags(adapter, config),
getWorkspaceSetting(adapter),
]);
const metrics = applyExposure(metricResult.rows, workspace, flags);
assertSafeResult(metrics);
await client.query("COMMIT");
inTransaction = false;
process.stdout.write(`${JSON.stringify({
status: "PASS",
transactionReadOnly: true,
sourceBuildSha: config.buildSha,
generatedAt: new Date().toISOString(),
authentication: authResult.rows[0],
modules: metrics,
}, null, 2)}\n`);
} catch {
if (inTransaction) {
try { await client.query("ROLLBACK"); } catch { /* fail closed */ }
}
process.stderr.write("[usage-census][FAILED] code=QUERY_FAILED\n");
process.exitCode = 1;
} finally {
await client.end().catch(() => {});
}
}
module.exports = {
AUTH_SQL,
MODULE_KEYS,
SAFE_RESULT_KEYS,
USAGE_SQL,
applyExposure,
assertSafeResult,
classifyFixtureActor,
effectiveFlagState,
isSmokeLikeValue,
};
// `node -` names stdin as argv[1] and leaves require.main unset. Production
// streams this task-scoped file through stdin so it never lands on the host.
if (require.main === module || process.argv[1] === "-") {
run();
}