Files
fusion/packages/core/src/plugin-activation-analytics.ts
gsxdsm 87f18f87ed FN-6705: add plugin activation analytics
Record plugin activation events and expose them to Command Center analytics.

- Add plugin activation persistence, aggregation helpers, and exports.
- Record load/reload activation events through the plugin loader and store APIs.
- Surface plugin activation counts in the Command Center Ecosystem area and API route.
- Cover migrations, aggregation behavior, plugin loader recording, route auth, UI rendering, and quarantined flaky suites.

Files changed:
 .changeset/fn-6705-plugin-activation-analytics.md  |   5 +
 docs/architecture.md                               |   2 +-
 docs/dashboard-guide.md                            |   2 +-
 packages/cli/vitest.config.ts                      |   8 +
 packages/core/src/__tests__/db-migrate.test.ts     |  34 ++---
 packages/core/src/__tests__/db.test.ts             |  59 ++++----
 packages/core/src/__tests__/goals-schema.test.ts   |   6 +-
 packages/core/src/__tests__/insight-store.test.ts  |  12 +-
 .../src/__tests__/merge-request-record.test.ts     |   3 +-
 packages/core/src/__tests__/mission-store.test.ts  |   6 +-
 .../__tests__/plugin-activation-analytics.test.ts  |  67 ++++++++
 packages/core/src/__tests__/plugin-loader.test.ts  | 168 ++++++++++++++++++++-
 packages/core/src/__tests__/run-audit.test.ts      |   6 +-
 .../core/src/__tests__/store-merge-queue.test.ts   |   3 +-
 .../src/__tests__/store-plugin-activations.test.ts |  52 +++++++
 packages/core/src/__tests__/task-documents.test.ts |   3 +-
 packages/core/src/db.ts                            |  35 ++++-
 packages/core/src/index.ts                         |   8 +
 packages/core/src/plugin-activation-analytics.ts   | 104 +++++++++++++
 packages/core/src/plugin-loader.ts                 |  27 ++++
 packages/core/src/store.ts                         |  24 ++-
 packages/core/src/types.ts                         |  22 +++
 packages/core/vitest.config.ts                     |  12 ++
 .../command-center/areas/EcosystemArea.tsx         |  40 +++--
 .../command-center/areas/__tests__/areas.test.tsx  |  52 ++++++-
 .../register-command-center-routes.auth.test.ts    |   1 +
 .../register-command-center-routes.test.ts         |  29 ++++
 .../src/routes/register-command-center-routes.ts   |  20 +++
 .../src/store/__tests__/roadmap-store.test.ts      |   9 +-
 scripts/boot-smoke.mjs                             |  24 ++-
 scripts/lib/test-quarantine.json                   |  55 +++++++
 31 files changed, 799 insertions(+), 99 deletions(-)

Fusion-Task-Id: FN-6705
Fusion-Task-Lineage: 12ed6227-f935-4038-bbe9-a605a054850e
2026-06-19 16:47:33 -07:00

105 lines
3.1 KiB
TypeScript

import type { Database } from "./db.js";
/**
* Plugin activation analytics over the project-scoped `plugin_activations` table.
*
* Fusion records one row when a plugin or workflow extension genuinely activates
* through `PluginLoader.loadPlugin` or `reloadPlugin`. The Command Center
* Ecosystem card may show a count only when at least one in-range row exists.
* Empty ranges return `unavailable: true` and `activations: 0` as a transport
* shape, but UI callers must keep the honest unavailable sentinel — never render
* `0` as if missing historical capture meant zero activations.
*
* Inclusivity: `from`/`to` bounds are inclusive and filter `activatedAt`.
*
* FNXC:CommandCenterEcosystem 2026-06-19-08:05:
* Plugin activation analytics are project-scoped event aggregates. Absence of rows means the metric is unavailable for the selected range, not that Fusion observed zero activations.
*/
export interface PluginActivationAnalyticsQuery {
/** ISO-8601 lower bound (inclusive). */
from?: string;
/** ISO-8601 upper bound (inclusive). */
to?: string;
}
/** Activation count for a single plugin id. */
export interface PluginActivationPluginCount {
pluginId: string;
count: number;
}
export interface PluginActivationAnalytics {
from: string | null;
to: string | null;
/** Real activation rows in range. */
activations: number;
/** Activation rows grouped by plugin id, descending by count. */
byPlugin: PluginActivationPluginCount[];
/** True when no in-range activation rows exist; UI should render the sentinel, not 0. */
unavailable: boolean;
}
interface CountRow {
count: number;
}
interface PluginCountRow {
pluginId: string;
count: number;
}
function rangeWhere(query: PluginActivationAnalyticsQuery): { where: string; params: string[] } {
const clauses: string[] = [];
const params: string[] = [];
if (query.from !== undefined) {
clauses.push("activatedAt >= ?");
params.push(query.from);
}
if (query.to !== undefined) {
clauses.push("activatedAt <= ?");
params.push(query.to);
}
return {
where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
params,
};
}
/**
* Aggregate plugin activations over a date range.
*
* Empty range yields `{ activations: 0, byPlugin: [], unavailable: true }` so
* callers can preserve the Command Center unavailable sentinel rather than
* fabricating a zero-valued metric.
*/
export function aggregatePluginActivations(
db: Database,
query: PluginActivationAnalyticsQuery = {},
): PluginActivationAnalytics {
const { where, params } = rangeWhere(query);
const activations = (
db
.prepare(`SELECT COUNT(*) AS count FROM plugin_activations ${where}`)
.get(...params) as CountRow
).count;
const byPlugin = db
.prepare(
`SELECT pluginId, COUNT(*) AS count
FROM plugin_activations ${where}
GROUP BY pluginId
ORDER BY count DESC, pluginId ASC`,
)
.all(...params) as PluginCountRow[];
return {
from: query.from ?? null,
to: query.to ?? null,
activations,
byPlugin,
unavailable: activations === 0,
};
}