1- // Yarn Berry plugin — records every yarn script execution in the local SQLite
2- // database (~/.tool-usage-collection/events.db) without modifying any script
3- // entries in package.json.
1+ // Yarn Berry plugin — records every yarn script execution to the local CSV
2+ // events log (~/.tool-usage-collection/metamask-mobile-events.log).
43//
5- // Delegates all tracking logic to scripts/tooling/tool-usage-collection.ts
6- // (via tsx) so there is a single source of truth for schema, DB access, and
7- // event writing .
4+ // Appends one CSV line per event directly via fs.appendFileSync — no spawning,
5+ // no tsx, no SQLite. The log is drained into the DB by dev-tooling-explorer
6+ // and the nightly cronjob when they start up .
87
98'use strict' ;
109
11- // CJS modules are wrapped in a function by Node.js, so `return` is valid here.
12- // Skip entirely in CI or when the developer has opted out — no hooks registered,
13- // no filesystem access, no spawning.
14- if ( process . env . CI || process . env . TOOL_USAGE_COLLECTION_OPT_IN === 'false' ) {
15- module . exports = { name : 'plugin-usage-tracking' , factory : ( ) => ( { } ) } ;
16- return ;
17- }
18-
19- const { spawn } = require ( 'child_process' ) ;
2010const fs = require ( 'fs' ) ;
2111const os = require ( 'os' ) ;
2212const path = require ( 'path' ) ;
2313
24- // Resolve paths relative to this plugin file so the plugin works correctly
25- // regardless of which directory yarn is invoked from (e.g. .github/scripts in CI).
26- const PLUGIN_DIR = path . dirname ( __filename ) ;
27- const REPO_ROOT = path . resolve ( PLUGIN_DIR , '..' , '..' ) ;
28- const TSX_BIN = path . join ( REPO_ROOT , 'node_modules' , '.bin' , 'tsx' ) ;
29- const COLLECTION_SCRIPT = path . join (
30- REPO_ROOT ,
31- 'scripts' ,
32- 'tooling' ,
33- 'tool-usage-collection.ts' ,
34- ) ;
35- const DEBUG_LOG = path . join ( os . homedir ( ) , '.tool-usage-collection' , 'plugin-debug.log' ) ;
14+ const PLUGIN_NAME = 'plugin-usage-tracking' ;
3615
37- function debugLog ( message ) {
38- try {
39- const dir = path . dirname ( DEBUG_LOG ) ;
40- if ( ! fs . existsSync ( dir ) ) fs . mkdirSync ( dir , { recursive : true } ) ;
41- fs . appendFileSync ( DEBUG_LOG , `[${ new Date ( ) . toISOString ( ) } ] ${ message } \n` ) ;
42- } catch {
43- // If we can't write the debug log either, there's nothing we can do
44- }
45- }
16+ function makeTrackingPlugin ( ) {
17+ const LOG_FILE =
18+ process . env . TOOL_USAGE_COLLECTION_LOG_PATH ||
19+ path . join ( os . homedir ( ) , '.tool-usage-collection' , 'metamask-mobile-events.log' ) ;
20+ const LOG_DIR = path . dirname ( LOG_FILE ) ;
4621
47- function track ( scriptName , eventType , extra ) {
48- const args = [
49- COLLECTION_SCRIPT ,
50- '--tool' , `yarn:${ scriptName } ` ,
51- '--type' , 'yarn_script' ,
52- '--event' , eventType ,
53- ] ;
54-
55- if ( extra ?. success != null ) {
56- args . push ( '--success' , String ( extra . success ) ) ;
57- }
58- if ( extra ?. duration_ms != null ) {
59- args . push ( '--duration' , String ( extra . duration_ms ) ) ;
60- }
22+ const DEBUG_LOG = path . join ( LOG_DIR , 'plugin-debug.log' ) ;
6123
62- // Guard: if tsx or the collection script are missing, skip silently.
63- // This happens when yarn is invoked from a subdirectory (e.g. .github/scripts in CI)
64- // where node_modules/.bin/tsx does not exist relative to that cwd.
65- if ( ! fs . existsSync ( TSX_BIN ) || ! fs . existsSync ( COLLECTION_SCRIPT ) ) {
66- debugLog (
67- `skipping tracking — tsx or script not found\n` +
68- ` tsx_bin=${ TSX_BIN } \n` +
69- ` script=${ COLLECTION_SCRIPT } ` ,
70- ) ;
71- return ;
24+ function debugLog ( message ) {
25+ try {
26+ if ( ! fs . existsSync ( LOG_DIR ) ) fs . mkdirSync ( LOG_DIR , { recursive : true } ) ;
27+ fs . appendFileSync ( DEBUG_LOG , `[${ new Date ( ) . toISOString ( ) } ] ${ message } \n` ) ;
28+ } catch {
29+ // Nothing we can do if even the debug log fails.
30+ }
7231 }
7332
74- // Fire-and-forget: detach immediately so the subprocess never blocks the
75- // user's terminal. The child writes its own DB errors to stderr (ignored
76- // here); spawn-level failures are logged to the debug log file.
77- const child = spawn ( TSX_BIN , args , {
78- detached : true ,
79- stdio : 'ignore' ,
80- cwd : REPO_ROOT ,
81- } ) ;
33+ // Format: tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at
34+ function appendEvent ( toolName , eventType , extra ) {
35+ const success = extra ?. success != null ? String ( extra . success ) : '' ;
36+ const durationMs = extra ?. duration_ms != null ? String ( extra . duration_ms ) : '' ;
37+ const timestamp = new Date ( ) . toISOString ( ) ;
8238
83- // Attach a no-op error handler to prevent unhandled 'error' events from
84- // crashing Yarn when spawn fails (e.g. ENOENT on the binary).
85- child . on ( 'error' , ( err ) => {
86- debugLog ( `spawn error: ${ err . message } ` ) ;
87- } ) ;
39+ // agent_vendor and session_id are always empty for Yarn plugin events.
40+ const line = `yarn:${ toolName } ,yarn_script,${ eventType } ,,,${ success } ,${ durationMs } ,${ timestamp } ` ;
8841
89- if ( child . pid === undefined ) {
90- debugLog (
91- `spawn FAILED — tsx not found\n` +
92- ` tsx_bin=${ TSX_BIN } \n` +
93- ` script=yarn:${ scriptName } event=${ eventType } ` ,
94- ) ;
95- return ;
42+ try {
43+ if ( ! fs . existsSync ( LOG_DIR ) ) fs . mkdirSync ( LOG_DIR , { recursive : true } ) ;
44+ // Write the header on first creation so the file is self-describing.
45+ if ( ! fs . existsSync ( LOG_FILE ) ) {
46+ fs . appendFileSync ( LOG_FILE , 'tool_name,tool_type,event_type,agent_vendor,session_id,success,duration_ms,created_at\n' ) ;
47+ }
48+ fs . appendFileSync ( LOG_FILE , line + '\n' ) ;
49+ } catch ( err ) {
50+ debugLog ( `append failed: ${ err . message } ` ) ;
51+ }
9652 }
9753
98- // Detach from the parent (Yarn) event loop — the child runs independently
99- child . unref ( ) ;
100- }
101-
102- module . exports = {
103- name : `plugin-usage-tracking` ,
104- factory : ( ) => ( {
54+ return {
10555 hooks : {
10656 // wrapScriptExecution signature:
10757 // (executor, project, locator, scriptName, extra) => Promise<() => Promise<number>>
10858 // The outer async resolves before the script runs; the inner async IS the script run.
10959 wrapScriptExecution : ( executor , _project , _locator , scriptName ) =>
11060 Promise . resolve ( async ( ) => {
111- track ( scriptName , 'start' ) ;
61+ appendEvent ( scriptName , 'start' ) ;
11262
11363 const start = Date . now ( ) ;
11464 let exitCode ;
@@ -119,7 +69,7 @@ module.exports = {
11969 // the user presses Ctrl+C). Record as 'interrupted' so the report can
12070 // distinguish abandoned sessions from genuine failures (success=0).
12171 const eventType = exitCode === 129 ? 'interrupted' : 'end' ;
122- track ( scriptName , eventType , {
72+ appendEvent ( scriptName , eventType , {
12373 success : exitCode === 129 ? undefined : exitCode === 0 ,
12474 duration_ms : Date . now ( ) - start ,
12575 } ) ;
@@ -128,5 +78,17 @@ module.exports = {
12878 return exitCode ;
12979 } ) ,
13080 } ,
131- } ) ,
81+ } ;
82+ }
83+
84+ module . exports = {
85+ name : PLUGIN_NAME ,
86+ factory : ( ) => {
87+ // Skip entirely in CI or when the developer has opted out — no hooks registered,
88+ // no filesystem access.
89+ if ( process . env . CI || process . env . TOOL_USAGE_COLLECTION_OPT_IN === 'false' ) {
90+ return { } ;
91+ }
92+ return makeTrackingPlugin ( ) ;
93+ } ,
13294} ;
0 commit comments