Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions .yarn/plugins/plugin-usage-tracking.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,74 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const childProcess = require('child_process');

const PLUGIN_NAME = 'plugin-usage-tracking';
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;

function makeTrackingPlugin() {
// Opportunistic once-a-day trigger: the first non-CI yarn script run after the
// 24h window spawns the anonymizer detached (non-blocking). The gate is the
// `last_run_at` field in anonymizer-state.json — the anonymizer re-checks and
// claims it, so concurrent spawns are harmless.
function maybeTriggerAnonymizer(repoRoot) {
try {
if (process.env.CI || process.env.TOOL_USAGE_COLLECTION_OPT_IN === 'false') {
return;
}

const statePath = path.join(
os.homedir(),
'.tool-usage-collection',
'anonymizer-state.json',
);
let lastRunAt = null;
if (fs.existsSync(statePath)) {
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
lastRunAt = parsed.last_run_at ?? null;
}

const nowMs = Date.now();
if (lastRunAt) {
const lastMs = Date.parse(lastRunAt);
if (!Number.isNaN(lastMs) && nowMs - lastMs < TWENTY_FOUR_HOURS_MS) {
return;
}
}

const binPath = path.join(
repoRoot,
'node_modules',
'@metamask',
'tooling-insight',
'dist',
'daily-anonymizer.mjs',
);
if (!fs.existsSync(binPath)) {
return;
}
childProcess.spawn(process.execPath, [binPath], {
detached: true,
stdio: 'ignore',
}).unref();
} catch (error) {
// Best-effort trigger — never throw into the yarn hot path.
try {
const logPath = path.join(os.homedir(), '.tool-usage-collection', 'anonymizer.log');
const entry = JSON.stringify({
ts: new Date().toISOString(),
cli: PLUGIN_NAME,
level: 'error',
message: 'maybeTriggerAnonymizer failed',
extra: { error: error instanceof Error ? error.message : String(error) },
});
fs.appendFileSync(logPath, `${entry}\n`);
} catch {
// Logging failed — nothing more we can do.
}
}
}

function makeTrackingPlugin(repoRoot) {
const LOG_FILE =
process.env.TOOL_USAGE_COLLECTION_LOG_PATH ||
path.join(os.homedir(), '.tool-usage-collection', 'metamask-mobile-events.log');
Expand Down Expand Up @@ -71,6 +135,7 @@ function makeTrackingPlugin() {
success: exitCode === 129 ? undefined : exitCode === 0,
duration_ms: Date.now() - start,
});
maybeTriggerAnonymizer(repoRoot);
}

return exitCode;
Expand All @@ -87,6 +152,6 @@ module.exports = {
if (process.env.CI || process.env.TOOL_USAGE_COLLECTION_OPT_IN === 'false') {
return {};
}
return makeTrackingPlugin();
return makeTrackingPlugin(process.cwd());
},
};
71 changes: 70 additions & 1 deletion .yarn/plugins/plugin-usage-tracking.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs';
import {
existsSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync,
} from 'fs';
import { spawn } from 'child_process';

/** Return only the data rows from the log file (skip the CSV header). */
function dataLines(file: string): string[] {
Expand Down Expand Up @@ -152,4 +160,65 @@ describe('plugin-usage-tracking', () => {
expect(existsSync(logFile)).toBe(false);
});
});

describe('maybeTriggerAnonymizer guard', () => {
let homeDir: string;
let savedHome: string | undefined;
let spawnSpy: jest.SpiedFunction<typeof spawn>;

beforeEach(() => {
spawnSpy = jest
.spyOn(require('child_process'), 'spawn')
.mockImplementation(
() => ({ unref: jest.fn() }) as ReturnType<typeof spawn>,
);
homeDir = mkdtempSync(join(tmpdir(), 'plugin-trigger-home-'));
savedHome = process.env.HOME;
process.env.HOME = homeDir;
delete process.env.CI;
mkdirSync(join(homeDir, '.tool-usage-collection'), { recursive: true });
});

afterEach(() => {
spawnSpy.mockRestore();
rmSync(homeDir, { recursive: true, force: true });
if (savedHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = savedHome;
}
});

async function runScript(scriptName: string): Promise<void> {
const { hooks } = plugin.factory();
const executor = jest.fn().mockResolvedValue(0);
const wrappedFactory = await hooks!.wrapScriptExecution(
executor,
null,
null,
scriptName,
);
await wrappedFactory();
}

it('does not spawn when last_run_at is within 24h', async () => {
writeFileSync(
join(homeDir, '.tool-usage-collection', 'anonymizer-state.json'),
JSON.stringify({
version: 1,
instance_uuid: 'uuid',
last_run_at: new Date().toISOString(),
last_pushed_day: null,
}),
);
await runScript('test:unit');
expect(spawnSpy).not.toHaveBeenCalled();
});

it('does not spawn when CI is set', () => {
process.env.CI = '1';
expect(plugin.factory()).toEqual({});
expect(spawnSpy).not.toHaveBeenCalled();
});
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@
"@metamask/test-dapp-bitcoin": "^0.2.0",
"@metamask/test-dapp-multichain": "^0.17.1",
"@metamask/test-dapp-solana": "^0.3.0",
"@metamask/tooling-insight": "npm:@metamask-previews/tooling-insight@1.0.1-preview-ff3ce0f",
"@octokit/rest": "^21.0.0",
"@open-rpc/mock-server": "^1.7.5",
"@open-rpc/schema-utils-js": "^1.16.2",
Expand Down
28 changes: 3 additions & 25 deletions scripts/tooling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,31 +116,9 @@ sqlite3 ~/.tool-usage-collection/events.db \

## Using dev-tooling-explorer

[dev-tooling-explorer](https://github.com/MetaMask/experimental-dev-tooling-explorer) is a local web UI for browsing the SQLite database written by the collection hooks. It lets you filter events by repo, tool, agent vendor, or date; view per-session history; explore usage charts; and prune or reset the database.
[dev-tooling-explorer](https://github.com/MetaMask/experimental-dev-tooling-explorer) is a local web UI for browsing the SQLite database written by the collection hooks. See its repo for installation and usage instructions.

### Installation
## Daily Anonymizer (MCWP-644)

```bash
git clone https://github.com/MetaMask/experimental-dev-tooling-explorer
cd experimental-dev-tooling-explorer
yarn install
```

### Usage

```bash
yarn start # opens local web UI
yarn start -- --port 4242 # fixed port
```

### Demo workflow (no collection setup needed)

```bash
yarn demo:generate
yarn start:demo
```

### DB path resolution

The explorer reads the database from `TOOL_USAGE_COLLECTION_DB_PATH` if set, otherwise defaults to `~/.tool-usage-collection/events.db`.
The daily anonymizer runs automatically once per 24h: the first non-CI `yarn` script run after the window opens spawns it detached (non-blocking) via the usage-tracking Yarn plugin. It is published as [`@metamask/tooling-insight`](https://github.com/MetaMask/experimental-tooling-insight) and consumed here as a `devDependency`. See that repo for full documentation, configuration, and ops escape hatches.

16 changes: 0 additions & 16 deletions scripts/tooling/tsconfig.json

This file was deleted.

Loading
Loading