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
105 lines
3.1 KiB
TypeScript
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,
|
|
};
|
|
}
|