@socketsecurity/cli-with-sentry 1.1.47 → 1.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.js CHANGED
@@ -23,10 +23,13 @@ var require$$13 = require('../external/@socketsecurity/registry/lib/url');
23
23
  var agent = require('../external/@socketsecurity/registry/lib/agent');
24
24
  var bin = require('../external/@socketsecurity/registry/lib/bin');
25
25
  var packages = require('../external/@socketsecurity/registry/lib/packages');
26
- var require$$0 = require('node:url');
26
+ var require$$0$1 = require('node:url');
27
27
  var globs = require('../external/@socketsecurity/registry/lib/globs');
28
28
  var streams = require('../external/@socketsecurity/registry/lib/streams');
29
29
  var promises = require('node:timers/promises');
30
+ var os = require('node:os');
31
+ var process$1 = require('node:process');
32
+ var require$$0 = require('node:crypto');
30
33
 
31
34
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
32
35
  /**
@@ -545,7 +548,7 @@ function updateConfigValue(configKey, value) {
545
548
  * - Used for permission validation and help text
546
549
  */
547
550
 
548
- const require$3 = require$$5.createRequire((typeof document === 'undefined' ? require$$0.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
551
+ const require$3 = require$$5.createRequire((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
549
552
  let _requirements;
550
553
  function getRequirements() {
551
554
  if (_requirements === undefined) {
@@ -561,6 +564,849 @@ function getRequirementsKey(cmdPath) {
561
564
  return cmdPath.replace(/^socket[: ]/, '').replace(/ +/g, ':');
562
565
  }
563
566
 
567
+ /**
568
+ * Telemetry service for Socket CLI.
569
+ * Manages event collection, batching, and submission to Socket API.
570
+ *
571
+ * IMPORTANT: Telemetry is ALWAYS scoped to an organization.
572
+ * Cannot track telemetry without an org context.
573
+ *
574
+ * Features:
575
+ * - Singleton pattern (one instance per process)
576
+ * - Organization-scoped tracking (required)
577
+ * - Event batching (auto-flush at batch size)
578
+ * - Exit handlers (auto-flush on process exit)
579
+ * - Automatic session ID assignment
580
+ * - Explicit finalization via destroy() for controlled cleanup
581
+ * - Graceful degradation (errors don't block CLI)
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * // Get telemetry client (returns singleton instance)
586
+ * const telemetry = await TelemetryService.getTelemetryClient('my-org')
587
+ *
588
+ * // Track an event (session_id is auto-set)
589
+ * telemetry.track({
590
+ * event_sender_created_at: new Date().toISOString(),
591
+ * event_type: 'cli_start',
592
+ * context: {
593
+ * version: '2.2.15',
594
+ * platform: process.platform,
595
+ * node_version: process.version,
596
+ * arch: process.arch,
597
+ * argv: process.argv.slice(2)
598
+ * }
599
+ * })
600
+ *
601
+ * // Flush happens automatically on batch size and exit
602
+ * // Can also be called manually if needed
603
+ * await telemetry.flush()
604
+ *
605
+ * // Always call destroy() before exit to flush remaining events
606
+ * await telemetry.destroy()
607
+ * ```
608
+ */
609
+
610
+ /**
611
+ * Debug wrapper for telemetry service.
612
+ * Wraps debugFn to provide a simpler API.
613
+ */
614
+ const debug$1 = message => {
615
+ require$$9.debugFn('socket:telemetry:service', message);
616
+ };
617
+
618
+ /**
619
+ * DebugDir wrapper for telemetry service.
620
+ */
621
+ const debugDirWrapper = obj => {
622
+ require$$9.debugDir('socket:telemetry:service', obj);
623
+ };
624
+
625
+ /**
626
+ * Process-wide session ID.
627
+ * Generated once per CLI invocation and shared across all telemetry instances.
628
+ */
629
+ const SESSION_ID = require$$0.randomUUID();
630
+
631
+ /**
632
+ * Default telemetry configuration.
633
+ * Used as fallback if API config fetch fails.
634
+ */
635
+ const DEFAULT_TELEMETRY_CONFIG = {
636
+ telemetry: {
637
+ enabled: false
638
+ }
639
+ };
640
+
641
+ /**
642
+ * Static configuration for telemetry service behavior.
643
+ */
644
+ const TELEMETRY_SERVICE_CONFIG = {
645
+ batch_size: 10,
646
+ // Auto-flush when queue reaches this size.
647
+ flush_timeout: 2_000 // 2 second maximum for flush operations.
648
+ };
649
+
650
+ /**
651
+ * Singleton instance holder.
652
+ */
653
+
654
+ /**
655
+ * Singleton telemetry service instance holder.
656
+ * Only one instance exists per process.
657
+ */
658
+ const telemetryServiceInstance = {
659
+ current: null
660
+ };
661
+
662
+ /**
663
+ * Wrap a promise with a timeout.
664
+ * Rejects if promise doesn't resolve within timeout.
665
+ *
666
+ * @param promise Promise to wrap.
667
+ * @param timeoutMs Timeout in milliseconds.
668
+ * @param errorMessage Error message if timeout occurs.
669
+ * @returns Promise that resolves or times out.
670
+ */
671
+ function withTimeout(promise, timeoutMs, errorMessage) {
672
+ return Promise.race([promise, new Promise((_, reject) => {
673
+ setTimeout(() => {
674
+ reject(new Error(errorMessage));
675
+ }, timeoutMs);
676
+ })]);
677
+ }
678
+
679
+ /**
680
+ * Centralized telemetry service for Socket CLI.
681
+ * Telemetry is always scoped to an organization.
682
+ * Singleton pattern ensures only one instance exists per process.
683
+ *
684
+ * NOTE: Only one telemetry instance exists per process.
685
+ * If getTelemetryClient() is called with a different organization slug,
686
+ * it returns the existing instance for the original organization.
687
+ * Switching organizations mid-execution is not supported - the first
688
+ * organization to initialize telemetry will be used for the entire process.
689
+ *
690
+ * This is intended, since we can't switch an org during command execution.
691
+ */
692
+ class TelemetryService {
693
+ config = null;
694
+ eventQueue = [];
695
+ isDestroyed = false;
696
+
697
+ /**
698
+ * Private constructor.
699
+ * Requires organization slug.
700
+ *
701
+ * @param orgSlug - Organization identifier.
702
+ */
703
+ constructor(orgSlug) {
704
+ this.orgSlug = orgSlug;
705
+ debug$1(`Telemetry service created for org '${orgSlug}' with session ID: ${SESSION_ID}`);
706
+ }
707
+
708
+ /**
709
+ * Get the current telemetry instance if one exists.
710
+ * Does not create a new instance.
711
+ *
712
+ * @returns Current telemetry instance or null if none exists.
713
+ */
714
+ static getCurrentInstance() {
715
+ return telemetryServiceInstance.current;
716
+ }
717
+
718
+ /**
719
+ * Get telemetry client for an organization.
720
+ * Creates and initializes client if it doesn't exist.
721
+ * Returns existing instance if already initialized.
722
+ *
723
+ * @param orgSlug - Organization identifier (required).
724
+ * @returns Initialized telemetry service instance.
725
+ */
726
+ static async getTelemetryClient(orgSlug) {
727
+ // Return existing instance if already initialized.
728
+ if (telemetryServiceInstance.current) {
729
+ debug$1(`Telemetry already initialized for org: ${telemetryServiceInstance.current.orgSlug}`);
730
+ return telemetryServiceInstance.current;
731
+ }
732
+ const instance = new TelemetryService(orgSlug);
733
+ try {
734
+ const sdkResult = await setupSdk();
735
+ if (!sdkResult.ok) {
736
+ debug$1('Failed to setup SDK for telemetry, using default config');
737
+ instance.config = DEFAULT_TELEMETRY_CONFIG;
738
+ telemetryServiceInstance.current = instance;
739
+ return instance;
740
+ }
741
+ const sdk = sdkResult.data;
742
+ const configResult = await sdk.getOrgTelemetryConfig(orgSlug);
743
+ if (configResult.success) {
744
+ instance.config = configResult.data;
745
+ debug$1(`Telemetry configuration fetched successfully: enabled=${instance.config.telemetry.enabled}`);
746
+ debugDirWrapper({
747
+ config: instance.config
748
+ });
749
+
750
+ // Periodic flush will start automatically when first event is tracked.
751
+ } else {
752
+ debug$1(`Failed to fetch telemetry config: ${configResult.error}`);
753
+ instance.config = DEFAULT_TELEMETRY_CONFIG;
754
+ }
755
+ } catch (e) {
756
+ debug$1(`Error initializing telemetry: ${e}`);
757
+ instance.config = DEFAULT_TELEMETRY_CONFIG;
758
+ }
759
+
760
+ // Only set singleton instance after full initialization.
761
+ telemetryServiceInstance.current = instance;
762
+ return instance;
763
+ }
764
+
765
+ /**
766
+ * Track a telemetry event.
767
+ * Adds event to queue for batching and eventual submission.
768
+ * Auto-flushes when batch size is reached.
769
+ *
770
+ * @param event - Telemetry event to track (session_id is optional and will be auto-set).
771
+ */
772
+ track(event) {
773
+ debug$1('Incoming track event request');
774
+ if (this.isDestroyed) {
775
+ debug$1('Telemetry service destroyed, ignoring event');
776
+ return;
777
+ }
778
+ if (!this.config?.telemetry.enabled) {
779
+ debug$1(`Telemetry disabled, skipping event: ${event.event_type}`);
780
+ return;
781
+ }
782
+
783
+ // Create complete event with session_id and org_slug.
784
+ const completeEvent = {
785
+ ...event,
786
+ session_id: SESSION_ID
787
+ };
788
+ debug$1(`Tracking telemetry event: ${completeEvent.event_type}`);
789
+ debugDirWrapper(completeEvent);
790
+ this.eventQueue.push(completeEvent);
791
+
792
+ // Auto-flush if batch size reached.
793
+ const batchSize = TELEMETRY_SERVICE_CONFIG.batch_size;
794
+ if (this.eventQueue.length >= batchSize) {
795
+ debug$1(`Batch size reached (${batchSize}), flushing events`);
796
+ void this.flush();
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Flush all queued events to the API.
802
+ * Returns immediately if no events queued or telemetry disabled.
803
+ * Times out after configured flush_timeout to prevent blocking CLI exit.
804
+ */
805
+ async flush() {
806
+ if (this.isDestroyed) {
807
+ debug$1('Telemetry service destroyed, cannot flush');
808
+ return;
809
+ }
810
+ if (this.eventQueue.length === 0) {
811
+ return;
812
+ }
813
+ if (!this.config?.telemetry.enabled) {
814
+ debug$1('Telemetry disabled, clearing queue without sending');
815
+ this.eventQueue = [];
816
+ return;
817
+ }
818
+ const eventsToSend = [...this.eventQueue];
819
+ this.eventQueue = [];
820
+ debug$1(`Flushing ${eventsToSend.length} telemetry events`);
821
+ const flushStartTime = Date.now();
822
+ try {
823
+ await withTimeout(this.sendEvents(eventsToSend), TELEMETRY_SERVICE_CONFIG.flush_timeout, `Telemetry flush timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`);
824
+ const flushDuration = Date.now() - flushStartTime;
825
+ debug$1(`Telemetry events sent successfully (${eventsToSend.length} events in ${flushDuration}ms)`);
826
+ } catch (e) {
827
+ const flushDuration = Date.now() - flushStartTime;
828
+ const errorMessage = e instanceof Error ? e.message : String(e);
829
+
830
+ // Check if this is a timeout error.
831
+ if (errorMessage.includes('timed out') || flushDuration >= TELEMETRY_SERVICE_CONFIG.flush_timeout) {
832
+ debug$1(`Telemetry flush timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`);
833
+ debug$1(`Failed to send ${eventsToSend.length} events due to timeout`);
834
+ } else {
835
+ debug$1(`Error flushing telemetry: ${errorMessage}`);
836
+ debug$1(`Failed to send ${eventsToSend.length} events due to error`);
837
+ }
838
+ // Events are discarded on error to prevent infinite growth.
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Send events to the API.
844
+ * Extracted as separate method for timeout wrapping.
845
+ *
846
+ * @param events Events to send.
847
+ */
848
+ async sendEvents(events) {
849
+ const sdkResult = await setupSdk();
850
+ if (!sdkResult.ok) {
851
+ debug$1('Failed to setup SDK for flush, events discarded');
852
+ return;
853
+ }
854
+ const sdk = sdkResult.data;
855
+
856
+ // Track flush statistics.
857
+ let successCount = 0;
858
+ let failureCount = 0;
859
+
860
+ // Send events in parallel for faster flush.
861
+ // Use allSettled to ensure all sends are attempted even if some fail.
862
+ const results = await Promise.allSettled(events.map(async event => {
863
+ const result = await sdk.postOrgTelemetry(this.orgSlug, event);
864
+ return {
865
+ event,
866
+ result
867
+ };
868
+ }));
869
+
870
+ // Log results and collect statistics.
871
+ for (const settledResult of results) {
872
+ if (settledResult.status === 'fulfilled') {
873
+ const {
874
+ event,
875
+ result
876
+ } = settledResult.value;
877
+ if (result.success) {
878
+ successCount++;
879
+ debug$1('Telemetry sent to telemetry:');
880
+ debugDirWrapper(event);
881
+ } else {
882
+ failureCount++;
883
+ debug$1(`Failed to send telemetry event: ${result.error}`);
884
+ }
885
+ } else {
886
+ failureCount++;
887
+ debug$1(`Telemetry request failed: ${settledResult.reason}`);
888
+ }
889
+ }
890
+
891
+ // Log flush statistics.
892
+ debug$1(`Flush stats: ${successCount} succeeded, ${failureCount} failed out of ${events.length} total`);
893
+ }
894
+
895
+ /**
896
+ * Destroy the telemetry service for this organization.
897
+ * Flushes remaining events and clears all state.
898
+ * Idempotent - safe to call multiple times.
899
+ */
900
+ async destroy() {
901
+ if (this.isDestroyed) {
902
+ debug$1('Telemetry service already destroyed, skipping');
903
+ return;
904
+ }
905
+ debug$1(`Destroying telemetry service for org: ${this.orgSlug}`);
906
+
907
+ // Mark as destroyed immediately to prevent concurrent destroy() calls.
908
+ this.isDestroyed = true;
909
+
910
+ // Flush remaining events with timeout.
911
+ const eventsToFlush = [...this.eventQueue];
912
+ this.eventQueue = [];
913
+ if (eventsToFlush.length > 0 && this.config?.telemetry.enabled) {
914
+ debug$1(`Flushing ${eventsToFlush.length} events before destroy`);
915
+ const flushStartTime = Date.now();
916
+ try {
917
+ await withTimeout(this.sendEvents(eventsToFlush), TELEMETRY_SERVICE_CONFIG.flush_timeout, `Telemetry flush during destroy timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`);
918
+ const flushDuration = Date.now() - flushStartTime;
919
+ debug$1(`Events flushed successfully during destroy (${flushDuration}ms)`);
920
+ } catch (e) {
921
+ const flushDuration = Date.now() - flushStartTime;
922
+ const errorMessage = e instanceof Error ? e.message : String(e);
923
+
924
+ // Check if this is a timeout error.
925
+ if (errorMessage.includes('timed out') || flushDuration >= TELEMETRY_SERVICE_CONFIG.flush_timeout) {
926
+ debug$1(`Telemetry flush during destroy timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`);
927
+ debug$1(`Failed to send ${eventsToFlush.length} events during destroy due to timeout`);
928
+ } else {
929
+ debug$1(`Error flushing telemetry during destroy: ${errorMessage}`);
930
+ debug$1(`Failed to send ${eventsToFlush.length} events during destroy due to error`);
931
+ }
932
+ }
933
+ }
934
+ this.config = null;
935
+
936
+ // Clear singleton instance.
937
+ telemetryServiceInstance.current = null;
938
+ debug$1(`Telemetry service destroyed for org: ${this.orgSlug}`);
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Telemetry integration helpers for Socket CLI.
944
+ * Provides utilities for tracking common CLI events and subprocess executions.
945
+ *
946
+ * Usage:
947
+ * ```typescript
948
+ * import {
949
+ * setupTelemetryExitHandlers,
950
+ * finalizeTelemetry,
951
+ * finalizeTelemetrySync,
952
+ * trackCliStart,
953
+ * trackCliEvent,
954
+ * trackCliComplete,
955
+ * trackCliError,
956
+ * trackSubprocessStart,
957
+ * trackSubprocessComplete,
958
+ * trackSubprocessError
959
+ * } from './utils/telemetry/integration.mts'
960
+ *
961
+ * // Set up exit handlers once during CLI initialization.
962
+ * setupTelemetryExitHandlers()
963
+ *
964
+ * // Track main CLI execution.
965
+ * const startTime = await trackCliStart(process.argv)
966
+ * await trackCliComplete(process.argv, startTime, 0)
967
+ *
968
+ * // Track custom event with optional metadata.
969
+ * await trackCliEvent('custom_event', process.argv, { key: 'value' })
970
+ *
971
+ * // Track subprocess/forked CLI execution.
972
+ * const subStart = await trackSubprocessStart('npm', { cwd: '/path' })
973
+ * await trackSubprocessComplete('npm', subStart, 0, { stdout_length: 1234 })
974
+ *
975
+ * // On subprocess error.
976
+ * await trackSubprocessError('npm', subStart, error, 1)
977
+ *
978
+ * // Manual finalization (usually not needed if exit handlers are set up).
979
+ * await finalizeTelemetry() // Async version.
980
+ * finalizeTelemetrySync() // Sync version (best-effort).
981
+ * ```
982
+ */
983
+ /**
984
+ * Debug wrapper for telemetry integration.
985
+ */
986
+ const debug = message => {
987
+ require$$9.debugFn('socket:telemetry:integration', message);
988
+ };
989
+
990
+ /**
991
+ * Finalize telemetry and clean up resources (async version).
992
+ * This should be called before process.exit to ensure telemetry is sent and resources are cleaned up.
993
+ * Use this in async contexts like beforeExit handlers.
994
+ *
995
+ * @returns Promise that resolves when finalization completes.
996
+ */
997
+ async function finalizeTelemetry() {
998
+ const instance = TelemetryService.getCurrentInstance();
999
+ if (instance) {
1000
+ debug('Flushing telemetry');
1001
+ await instance.flush();
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Finalize telemetry synchronously (best-effort).
1007
+ * This triggers a flush without awaiting it.
1008
+ * Use this in synchronous contexts like signal handlers where async operations are not possible.
1009
+ *
1010
+ * Note: This is best-effort only. Events may be lost if the process exits before flush completes.
1011
+ * Prefer finalizeTelemetry() (async version) when possible.
1012
+ */
1013
+ function finalizeTelemetrySync() {
1014
+ const instance = TelemetryService.getCurrentInstance();
1015
+ if (instance) {
1016
+ debug('Triggering sync flush (best-effort)');
1017
+ void instance.flush();
1018
+ }
1019
+ }
1020
+
1021
+ // Track whether exit handlers have been set up to prevent duplicate registration.
1022
+ let exitHandlersRegistered = false;
1023
+
1024
+ /**
1025
+ * Set up exit handlers for telemetry finalization.
1026
+ * This registers handlers for both normal exits (beforeExit) and common fatal signals.
1027
+ *
1028
+ * Flushing strategy:
1029
+ * - Batch-based: Auto-flush when queue reaches 10 events.
1030
+ * - beforeExit: Async handler for clean shutdowns (when event loop empties).
1031
+ * - Fatal signals (SIGINT, SIGTERM, SIGHUP): Best-effort sync flush.
1032
+ * - Accepts that forced exits (SIGKILL, process.exit()) may lose final events.
1033
+ *
1034
+ * Call this once during CLI initialization to ensure telemetry is flushed on exit.
1035
+ * Safe to call multiple times - only registers handlers once.
1036
+ *
1037
+ * @example
1038
+ * ```typescript
1039
+ * // In src/cli.mts
1040
+ * setupTelemetryExitHandlers()
1041
+ * ```
1042
+ */
1043
+ function setupTelemetryExitHandlers() {
1044
+ // Prevent duplicate handler registration.
1045
+ if (exitHandlersRegistered) {
1046
+ debug('Telemetry exit handlers already registered, skipping');
1047
+ return;
1048
+ }
1049
+ exitHandlersRegistered = true;
1050
+
1051
+ // Use beforeExit for async finalization during clean shutdowns.
1052
+ // This fires when the event loop empties but before process actually exits.
1053
+ process$1.on('beforeExit', () => {
1054
+ debug('beforeExit handler triggered');
1055
+ void finalizeTelemetry();
1056
+ });
1057
+
1058
+ // Register handlers for common fatal signals as best-effort fallback.
1059
+ // These are synchronous contexts, so we can only trigger flush without awaiting.
1060
+ const fatalSignals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
1061
+ for (const signal of fatalSignals) {
1062
+ try {
1063
+ process$1.on(signal, () => {
1064
+ debug(`Signal ${signal} received, attempting sync flush`);
1065
+ finalizeTelemetrySync();
1066
+ });
1067
+ } catch (e) {
1068
+ // Some signals may not be available on all platforms.
1069
+ debug(`Failed to register handler for signal ${signal}: ${e}`);
1070
+ }
1071
+ }
1072
+ debug('Telemetry exit handlers registered (beforeExit + common signals)');
1073
+ }
1074
+
1075
+ /**
1076
+ * Track subprocess exit and finalize telemetry.
1077
+ * This is a convenience function that tracks completion/error based on exit code
1078
+ * and ensures telemetry is flushed before returning.
1079
+ *
1080
+ * Note: Only tracks subprocess-level events. CLI-level events (cli_complete, cli_error)
1081
+ * are tracked by the main CLI entry point in src/cli.mts.
1082
+ *
1083
+ * @param command - Command name (e.g., 'npm', 'pip').
1084
+ * @param startTime - Start timestamp from trackSubprocessStart.
1085
+ * @param exitCode - Process exit code (null treated as error).
1086
+ * @returns Promise that resolves when tracking and flush complete.
1087
+ *
1088
+ * @example
1089
+ * ```typescript
1090
+ * await trackSubprocessExit(NPM, subprocessStartTime, code)
1091
+ * ```
1092
+ */
1093
+ async function trackSubprocessExit(command, startTime, exitCode) {
1094
+ // Track subprocess completion or error based on exit code.
1095
+ if (exitCode !== null && exitCode !== 0) {
1096
+ const error = new Error(`${command} exited with code ${exitCode}`);
1097
+ await trackSubprocessError(command, startTime, error, exitCode);
1098
+ } else if (exitCode === 0) {
1099
+ await trackSubprocessComplete(command, startTime, exitCode);
1100
+ }
1101
+
1102
+ // Flush telemetry to ensure events are sent before exit.
1103
+ await finalizeTelemetry();
1104
+ }
1105
+
1106
+ // Add other subcommands
1107
+ const WRAPPER_CLI = new Set(['bun', 'npm', 'npx', 'pip', 'pnpm', 'vlt', 'yarn']);
1108
+
1109
+ // Add other sensitive flags
1110
+ const API_TOKEN_FLAGS = new Set(['--api-token', '--token', '-t']);
1111
+
1112
+ /**
1113
+ * Calculate duration from start timestamp.
1114
+ *
1115
+ * @param startTime - Start timestamp from Date.now().
1116
+ * @returns Duration in milliseconds.
1117
+ */
1118
+ function calculateDuration(startTime) {
1119
+ return Date.now() - startTime;
1120
+ }
1121
+
1122
+ /**
1123
+ * Normalize exit code to a number with default fallback.
1124
+ *
1125
+ * @param exitCode - Exit code (may be string, number, null, or undefined).
1126
+ * @param defaultValue - Default value if exitCode is not a number.
1127
+ * @returns Normalized exit code.
1128
+ */
1129
+ function normalizeExitCode(exitCode, defaultValue) {
1130
+ return typeof exitCode === 'number' ? exitCode : defaultValue;
1131
+ }
1132
+
1133
+ /**
1134
+ * Normalize error to Error object.
1135
+ *
1136
+ * @param error - Unknown error value.
1137
+ * @returns Error object.
1138
+ */
1139
+ function normalizeError(error) {
1140
+ return error instanceof Error ? error : new Error(String(error));
1141
+ }
1142
+
1143
+ /**
1144
+ * Build context for the current telemetry entry.
1145
+ *
1146
+ * The context contains the current execution context, in which all CLI invocation should have access to.
1147
+ *
1148
+ * @param argv Command line arguments.
1149
+ * @returns Telemetry context object.
1150
+ */
1151
+ function buildContext(argv) {
1152
+ return {
1153
+ arch: process$1.arch,
1154
+ argv: sanitizeArgv(argv),
1155
+ node_version: process$1.version,
1156
+ platform: process$1.platform,
1157
+ version: constants.default.ENV.INLINED_SOCKET_CLI_VERSION
1158
+ };
1159
+ }
1160
+
1161
+ /**
1162
+ * Sanitize argv to remove sensitive information.
1163
+ * Removes API tokens, file paths with usernames, and other PII.
1164
+ * Also strips arguments after wrapper CLIs to avoid leaking package names.
1165
+ *
1166
+ * @param argv Raw command line arguments (full process.argv including execPath and script).
1167
+ * @returns Sanitized argv array.
1168
+ *
1169
+ * @example
1170
+ * // Input: ['node', 'socket', 'npm', 'install', '@my/private-package', '--token', 'sktsec_abc123']
1171
+ * // Output: ['npm', 'install']
1172
+ */
1173
+ function sanitizeArgv(argv) {
1174
+ // Strip the first two values to drop the execPath and script.
1175
+ const withoutPathAndScript = argv.slice(2);
1176
+
1177
+ // Then strip arguments after wrapper CLIs to avoid leaking package names.
1178
+ const wrapperIndex = withoutPathAndScript.findIndex(arg => WRAPPER_CLI.has(arg));
1179
+ let strippedArgv = withoutPathAndScript;
1180
+ if (wrapperIndex !== -1) {
1181
+ // Keep only wrapper + first command (e.g., ['npm']).
1182
+ const endIndex = wrapperIndex + 1;
1183
+ strippedArgv = withoutPathAndScript.slice(0, endIndex);
1184
+ }
1185
+
1186
+ // Then sanitize remaining arguments.
1187
+ return strippedArgv.map((arg, index) => {
1188
+ // Check if previous arg was an API token flag.
1189
+ if (index > 0) {
1190
+ const prevArg = strippedArgv[index - 1];
1191
+ if (prevArg && API_TOKEN_FLAGS.has(prevArg)) {
1192
+ return '[REDACTED]';
1193
+ }
1194
+ }
1195
+
1196
+ // Redact anything that looks like a socket API token.
1197
+ if (arg.startsWith('sktsec_') || arg.match(/^[a-f0-9]{32,}$/i)) {
1198
+ return '[REDACTED]';
1199
+ }
1200
+
1201
+ // Remove user home directory from file paths.
1202
+ const homeDir = os.homedir();
1203
+ if (homeDir) {
1204
+ return arg.replace(new RegExp(regexps.escapeRegExp(homeDir), 'g'), '~');
1205
+ }
1206
+ return arg;
1207
+ });
1208
+ }
1209
+
1210
+ /**
1211
+ * Sanitize error attribute to remove user specific paths.
1212
+ * Replaces user home directory and other sensitive paths.
1213
+ *
1214
+ * @param input Raw input.
1215
+ * @returns Sanitized input.
1216
+ */
1217
+ function sanitizeErrorAttribute(input) {
1218
+ if (!input) {
1219
+ return undefined;
1220
+ }
1221
+
1222
+ // Remove user home directory.
1223
+ const homeDir = os.homedir();
1224
+ if (homeDir) {
1225
+ return input.replace(new RegExp(regexps.escapeRegExp(homeDir), 'g'), '~');
1226
+ }
1227
+ return input;
1228
+ }
1229
+
1230
+ /**
1231
+ * Generic event tracking function.
1232
+ * Tracks any telemetry event with optional error details and explicit flush.
1233
+ *
1234
+ * Events are automatically flushed via batch size or exit handlers.
1235
+ * Use the flush option only when immediate submission is required.
1236
+ *
1237
+ * @param eventType Type of event to track.
1238
+ * @param context Event context.
1239
+ * @param metadata Event metadata.
1240
+ * @param options Optional configuration.
1241
+ * @returns Promise that resolves when tracking completes.
1242
+ */
1243
+ async function trackEvent(eventType, context, metadata = {}, options = {}) {
1244
+ // Skip telemetry in test environments.
1245
+ if (constants.default.ENV.VITEST) {
1246
+ return;
1247
+ }
1248
+ try {
1249
+ const orgSlug = getConfigValueOrUndef(constants.CONFIG_KEY_DEFAULT_ORG);
1250
+ if (orgSlug) {
1251
+ const telemetry = await TelemetryService.getTelemetryClient(orgSlug);
1252
+ debug(`Got telemetry service for org: ${orgSlug}`);
1253
+ const event = {
1254
+ context,
1255
+ event_sender_created_at: new Date().toISOString(),
1256
+ event_type: eventType,
1257
+ ...(Object.keys(metadata).length > 0 && {
1258
+ metadata
1259
+ }),
1260
+ ...(options.error && {
1261
+ error: {
1262
+ message: sanitizeErrorAttribute(options.error.message),
1263
+ stack: sanitizeErrorAttribute(options.error.stack),
1264
+ type: options.error.constructor.name
1265
+ }
1266
+ })
1267
+ };
1268
+ telemetry.track(event);
1269
+
1270
+ // Flush events if requested.
1271
+ if (options.flush) {
1272
+ await telemetry.flush();
1273
+ }
1274
+ }
1275
+ } catch (err) {
1276
+ // Telemetry errors should never block CLI execution.
1277
+ debug(`Failed to track event ${eventType}: ${err}`);
1278
+ }
1279
+ }
1280
+
1281
+ /**
1282
+ * Track CLI initialization event.
1283
+ * Should be called at the start of CLI execution.
1284
+ *
1285
+ * @param argv Command line arguments (process.argv).
1286
+ * @returns Start timestamp for duration calculation.
1287
+ */
1288
+ async function trackCliStart(argv) {
1289
+ debug('Capture start of command');
1290
+ const startTime = Date.now();
1291
+ await trackEvent('cli_start', buildContext(argv));
1292
+ return startTime;
1293
+ }
1294
+
1295
+ /**
1296
+ * Track a generic CLI event with optional metadata.
1297
+ * Use this for tracking custom events during CLI execution.
1298
+ *
1299
+ * @param eventType Type of event to track.
1300
+ * @param argv Command line arguments (process.argv).
1301
+ * @param metadata Optional additional metadata to include with the event.
1302
+ */
1303
+ async function trackCliEvent(eventType, argv, metadata) {
1304
+ debug(`Tracking CLI event: ${eventType}`);
1305
+ await trackEvent(eventType, buildContext(argv), metadata);
1306
+ }
1307
+
1308
+ /**
1309
+ * Track CLI completion event.
1310
+ * Should be called on successful CLI exit.
1311
+ * Flushes immediately since this is typically the last event before process exit.
1312
+ *
1313
+ * @param argv
1314
+ * @param startTime Start timestamp from trackCliStart.
1315
+ * @param exitCode Process exit code (default: 0).
1316
+ */
1317
+ async function trackCliComplete(argv, startTime, exitCode) {
1318
+ debug('Capture end of command');
1319
+ await trackEvent('cli_complete', buildContext(argv), {
1320
+ duration: calculateDuration(startTime),
1321
+ exit_code: normalizeExitCode(exitCode, 0)
1322
+ }, {
1323
+ flush: true
1324
+ });
1325
+ }
1326
+
1327
+ /**
1328
+ * Track CLI error event.
1329
+ * Should be called when CLI exits with an error.
1330
+ * Flushes immediately since this is typically the last event before process exit.
1331
+ *
1332
+ * @param argv
1333
+ * @param startTime Start timestamp from trackCliStart.
1334
+ * @param error Error that occurred.
1335
+ * @param exitCode Process exit code (default: 1).
1336
+ */
1337
+ async function trackCliError(argv, startTime, error, exitCode) {
1338
+ debug('Capture error and stack trace of command');
1339
+ await trackEvent('cli_error', buildContext(argv), {
1340
+ duration: calculateDuration(startTime),
1341
+ exit_code: normalizeExitCode(exitCode, 1)
1342
+ }, {
1343
+ error: normalizeError(error),
1344
+ flush: true
1345
+ });
1346
+ }
1347
+
1348
+ /**
1349
+ * Track subprocess/command start event.
1350
+ *
1351
+ * Use this when spawning external commands like npm, npx, coana, cdxgen, etc.
1352
+ *
1353
+ * @param command Command being executed (e.g., 'npm', 'npx', 'coana').
1354
+ * @param metadata Optional additional metadata (e.g., cwd, purpose).
1355
+ * @returns Start timestamp for duration calculation.
1356
+ */
1357
+ async function trackSubprocessStart(command, metadata) {
1358
+ debug(`Tracking subprocess start: ${command}`);
1359
+ const startTime = Date.now();
1360
+ await trackEvent('subprocess_start', buildContext(process$1.argv), {
1361
+ command,
1362
+ ...metadata
1363
+ });
1364
+ return startTime;
1365
+ }
1366
+
1367
+ /**
1368
+ * Track subprocess/command completion event.
1369
+ *
1370
+ * Should be called when spawned command completes successfully.
1371
+ *
1372
+ * @param command Command that was executed.
1373
+ * @param startTime Start timestamp from trackSubprocessStart.
1374
+ * @param exitCode Process exit code.
1375
+ * @param metadata Optional additional metadata (e.g., stdout length, stderr length).
1376
+ */
1377
+ async function trackSubprocessComplete(command, startTime, exitCode, metadata) {
1378
+ debug(`Tracking subprocess complete: ${command}`);
1379
+ await trackEvent('subprocess_complete', buildContext(process$1.argv), {
1380
+ command,
1381
+ duration: calculateDuration(startTime),
1382
+ exit_code: normalizeExitCode(exitCode, 0),
1383
+ ...metadata
1384
+ });
1385
+ }
1386
+
1387
+ /**
1388
+ * Track subprocess/command error event.
1389
+ *
1390
+ * Should be called when spawned command fails or throws error.
1391
+ *
1392
+ * @param command Command that was executed.
1393
+ * @param startTime Start timestamp from trackSubprocessStart.
1394
+ * @param error Error that occurred.
1395
+ * @param exitCode Process exit code.
1396
+ * @param metadata Optional additional metadata.
1397
+ */
1398
+ async function trackSubprocessError(command, startTime, error, exitCode, metadata) {
1399
+ debug(`Tracking subprocess error: ${command}`);
1400
+ await trackEvent('subprocess_error', buildContext(process$1.argv), {
1401
+ command,
1402
+ duration: calculateDuration(startTime),
1403
+ exit_code: normalizeExitCode(exitCode, 1),
1404
+ ...metadata
1405
+ }, {
1406
+ error: normalizeError(error)
1407
+ });
1408
+ }
1409
+
564
1410
  /**
565
1411
  * Socket SDK utilities for Socket CLI.
566
1412
  * Manages SDK initialization and configuration for API communication.
@@ -673,17 +1519,54 @@ async function setupSdk(options) {
673
1519
  version: constants.default.ENV.INLINED_SOCKET_CLI_VERSION,
674
1520
  homepage: constants.default.ENV.INLINED_SOCKET_CLI_HOMEPAGE
675
1521
  }),
676
- // Add HTTP request hooks for debugging if SOCKET_CLI_DEBUG is enabled.
677
- ...(constants.default.ENV.SOCKET_CLI_DEBUG ? {
678
- hooks: {
679
- onRequest: info => {
1522
+ // Add HTTP request hooks for telemetry and debugging.
1523
+ hooks: {
1524
+ onRequest: info => {
1525
+ // Skip tracking for telemetry submission endpoints to prevent infinite loop.
1526
+ const isTelemetryEndpoint = info.url.includes('/telemetry');
1527
+ if (constants.default.ENV.SOCKET_CLI_DEBUG) {
1528
+ // Debug logging.
680
1529
  debugApiRequest(info.method, info.url, info.timeout);
681
- },
682
- onResponse: info => {
1530
+ }
1531
+ if (!isTelemetryEndpoint) {
1532
+ // Track API request event.
1533
+ void trackCliEvent('api_request', process.argv, {
1534
+ method: info.method,
1535
+ timeout: info.timeout,
1536
+ url: info.url
1537
+ });
1538
+ }
1539
+ },
1540
+ onResponse: info => {
1541
+ // Skip tracking for telemetry submission endpoints to prevent infinite loop.
1542
+ const isTelemetryEndpoint = info.url.includes('/telemetry');
1543
+ if (!isTelemetryEndpoint) {
1544
+ // Track API response event.
1545
+ const metadata = {
1546
+ duration: info.duration,
1547
+ method: info.method,
1548
+ status: info.status,
1549
+ statusText: info.statusText,
1550
+ url: info.url
1551
+ };
1552
+ if (info.error) {
1553
+ // Track as error event if request failed.
1554
+ void trackCliEvent('api_error', process.argv, {
1555
+ ...metadata,
1556
+ error_message: info.error.message,
1557
+ error_type: info.error.constructor.name
1558
+ });
1559
+ } else {
1560
+ // Track as successful response.
1561
+ void trackCliEvent('api_response', process.argv, metadata);
1562
+ }
1563
+ }
1564
+ if (constants.default.ENV.SOCKET_CLI_DEBUG) {
1565
+ // Debug logging.
683
1566
  debugApiResponse(info.method, info.url, info.status, info.error, info.duration, info.headers);
684
1567
  }
685
1568
  }
686
- } : {})
1569
+ }
687
1570
  };
688
1571
  if (constants.default.ENV.SOCKET_CLI_DEBUG) {
689
1572
  logger.logger.info(`[DEBUG] ${new Date().toISOString()} SDK options: ${JSON.stringify(sdkOptions)}`);
@@ -3460,7 +4343,7 @@ function isYarnBerry() {
3460
4343
  * - Configures environment for third-party tools
3461
4344
  */
3462
4345
 
3463
- const require$2 = require$$5.createRequire((typeof document === 'undefined' ? require$$0.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
4346
+ const require$2 = require$$5.createRequire((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
3464
4347
  const {
3465
4348
  PACKAGE_LOCK_JSON,
3466
4349
  PNPM_LOCK_YAML,
@@ -4329,10 +5212,11 @@ function isPnpmLockfileScanCommand(command) {
4329
5212
  * - PURL_Type: Package URL type from Socket SDK
4330
5213
  *
4331
5214
  * Supported Ecosystems:
4332
- * - apk, bitbucket, cargo, chrome, cocoapods, composer
5215
+ * - alpm, apk, bitbucket, cargo, chrome, cocoapods, composer
4333
5216
  * - conan, conda, cran, deb, docker, gem, generic
4334
5217
  * - github, gitlab, go, hackage, hex, huggingface
4335
- * - maven, mlflow, npm, nuget, oci, pub, pypi, rpm, swift
5218
+ * - maven, mlflow, npm, nuget, oci, pub, pypi, qpkg, rpm
5219
+ * - swift, swid, unknown, vscode
4336
5220
  *
4337
5221
  * Usage:
4338
5222
  * - Validates ecosystem types
@@ -4340,7 +5224,15 @@ function isPnpmLockfileScanCommand(command) {
4340
5224
  * - Ensures type safety for ecosystem operations
4341
5225
  */
4342
5226
 
4343
- const ALL_ECOSYSTEMS = ['apk', 'bitbucket', 'cargo', 'chrome', 'cocoapods', 'composer', 'conan', 'conda', 'cran', 'deb', 'docker', 'gem', 'generic', 'github', 'golang', 'hackage', 'hex', 'huggingface', 'maven', 'mlflow', constants.NPM, 'nuget', 'oci', 'pub', 'pypi', 'qpkg', 'rpm', 'swift', 'swid', 'unknown'];
5227
+
5228
+ // Temporarily commented out due to dependency version mismatch.
5229
+ // SDK has "alpm" but registry's EcosystemString doesn't yet.
5230
+ // type MissingInEcosystemString = Exclude<PURL_Type, EcosystemString>
5231
+
5232
+ // export type _Check_EcosystemString_has_all_purl_types =
5233
+ // ExpectNever<MissingInEcosystemString>
5234
+
5235
+ const ALL_ECOSYSTEMS = ['alpm', 'apk', 'bitbucket', 'cargo', 'chrome', 'cocoapods', 'composer', 'conan', 'conda', 'cran', 'deb', 'docker', 'gem', 'generic', 'github', 'golang', 'hackage', 'hex', 'huggingface', 'maven', 'mlflow', constants.NPM, 'nuget', 'oci', 'pub', 'pypi', 'qpkg', 'rpm', 'swift', 'swid', 'unknown', 'vscode'];
4344
5236
  new Set(ALL_ECOSYSTEMS);
4345
5237
  function getEcosystemChoicesForMeow() {
4346
5238
  return [...ALL_ECOSYSTEMS];
@@ -5191,7 +6083,7 @@ function isPnpmBinPathShadowed() {
5191
6083
  * - Preserves original binary functionality
5192
6084
  */
5193
6085
 
5194
- const __filename$1 = require$$0.fileURLToPath((typeof document === 'undefined' ? require$$0.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
6086
+ const __filename$1 = require$$0$1.fileURLToPath((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
5195
6087
  const __dirname$1 = path.dirname(__filename$1);
5196
6088
  async function installNpmLinks(shadowBinPath) {
5197
6089
  // Find npm being shadowed by this process.
@@ -5472,7 +6364,7 @@ class ColorOrMarkdown {
5472
6364
  }
5473
6365
  }
5474
6366
 
5475
- const require$1 = require$$5.createRequire((typeof document === 'undefined' ? require$$0.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
6367
+ const require$1 = require$$5.createRequire((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
5476
6368
  let _translations;
5477
6369
  function getTranslations() {
5478
6370
  if (_translations === undefined) {
@@ -6124,6 +7016,7 @@ exports.fetchGhsaDetails = fetchGhsaDetails;
6124
7016
  exports.fetchOrganization = fetchOrganization;
6125
7017
  exports.fileLink = fileLink;
6126
7018
  exports.filterFlags = filterFlags;
7019
+ exports.finalizeTelemetry = finalizeTelemetry;
6127
7020
  exports.findUp = findUp;
6128
7021
  exports.formatErrorWithDetail = formatErrorWithDetail;
6129
7022
  exports.getAlertsMapFromPnpmLockfile = getAlertsMapFromPnpmLockfile;
@@ -6207,6 +7100,7 @@ exports.sendApiRequest = sendApiRequest;
6207
7100
  exports.serializeResultJson = serializeResultJson;
6208
7101
  exports.setGitRemoteGithubRepoUrl = setGitRemoteGithubRepoUrl;
6209
7102
  exports.setupSdk = setupSdk;
7103
+ exports.setupTelemetryExitHandlers = setupTelemetryExitHandlers;
6210
7104
  exports.socketDashboardLink = socketDashboardLink;
6211
7105
  exports.socketDevLink = socketDevLink;
6212
7106
  exports.socketDocsLink = socketDocsLink;
@@ -6217,9 +7111,14 @@ exports.spawnSynpDlx = spawnSynpDlx;
6217
7111
  exports.suggestOrgSlug = suggestOrgSlug;
6218
7112
  exports.tildify = tildify;
6219
7113
  exports.toFilterConfig = toFilterConfig;
7114
+ exports.trackCliComplete = trackCliComplete;
7115
+ exports.trackCliError = trackCliError;
7116
+ exports.trackCliStart = trackCliStart;
7117
+ exports.trackSubprocessExit = trackSubprocessExit;
7118
+ exports.trackSubprocessStart = trackSubprocessStart;
6220
7119
  exports.updateConfigValue = updateConfigValue;
6221
7120
  exports.walkNestedMap = walkNestedMap;
6222
7121
  exports.webLink = webLink;
6223
7122
  exports.writeSocketJson = writeSocketJson;
6224
- //# debugId=16acc98b-82db-4f47-b14e-cc4d4625d581
7123
+ //# debugId=a083299b-d999-403d-b54b-00740d629c69
6225
7124
  //# sourceMappingURL=utils.js.map