git-watchtower 1.6.0 → 1.7.0

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * PostHog analytics wrapper (singleton)
3
+ *
4
+ * All methods are safe no-ops when telemetry is disabled.
5
+ * Events are fire-and-forget — never blocks the TUI.
6
+ */
7
+
8
+ const { isTelemetryEnabled, getOrCreateDistinctId } = require('./config');
9
+
10
+ const POSTHOG_API_KEY = 'phc_fdGL8TVN5aFPXmQ4f1hI8y6sqnscD7dy9j5SM5gTylG';
11
+ const POSTHOG_HOST = 'https://us.i.posthog.com';
12
+
13
+ /** @type {import('posthog-node').PostHog | null} */
14
+ let client = null;
15
+ let distinctId = '';
16
+ let appVersion = '';
17
+ let enabled = false;
18
+
19
+ /**
20
+ * Initialize the PostHog client if telemetry is enabled
21
+ * @param {{ version: string }} options
22
+ */
23
+ function init({ version }) {
24
+ appVersion = version;
25
+
26
+ if (!isTelemetryEnabled()) {
27
+ enabled = false;
28
+ return;
29
+ }
30
+
31
+ try {
32
+ const { PostHog } = require('posthog-node');
33
+ distinctId = getOrCreateDistinctId();
34
+ client = new PostHog(POSTHOG_API_KEY, {
35
+ host: POSTHOG_HOST,
36
+ flushAt: 10,
37
+ flushInterval: 30000,
38
+ requestTimeout: 5000,
39
+ disableGeoip: true,
40
+ });
41
+ enabled = true;
42
+ } catch {
43
+ // If posthog-node fails to load, silently disable telemetry
44
+ enabled = false;
45
+ client = null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Capture an analytics event (fire-and-forget)
51
+ * @param {string} event - Event name
52
+ * @param {Record<string, any>} [properties] - Event properties
53
+ */
54
+ function capture(event, properties = {}) {
55
+ if (!enabled || !client) return;
56
+
57
+ try {
58
+ client.capture({
59
+ distinctId,
60
+ event,
61
+ properties: {
62
+ ...properties,
63
+ $lib: 'git-watchtower',
64
+ $lib_version: appVersion,
65
+ },
66
+ });
67
+ } catch {
68
+ // Never let telemetry errors affect the app
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Capture an error for PostHog error tracking
74
+ * @param {Error} error
75
+ */
76
+ function captureError(error) {
77
+ if (!enabled || !client) return;
78
+
79
+ try {
80
+ const errorType = error.constructor?.name || 'Error';
81
+ const errorCode = /** @type {any} */ (error).code || '';
82
+ const errorMessage = errorCode || (error.message || '').substring(0, 200);
83
+
84
+ /** @type {Record<string, any>} */
85
+ const properties = {
86
+ $exception_type: errorType,
87
+ $exception_message: errorMessage,
88
+ $exception_source: 'node',
89
+ $lib: 'git-watchtower',
90
+ $lib_version: appVersion,
91
+ };
92
+
93
+ // Include stack trace — contains only our package's file paths and
94
+ // line numbers, no user data. Required for PostHog error tracking
95
+ // to group, deduplicate, and show useful backtraces.
96
+ if (error.stack) {
97
+ properties.$exception_stack_trace_raw = error.stack;
98
+ }
99
+
100
+ client.capture({
101
+ distinctId,
102
+ event: '$exception',
103
+ properties,
104
+ });
105
+ } catch {
106
+ // Never let telemetry errors affect the app
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Flush pending events and shutdown the PostHog client
112
+ * Call this before process exit to ensure events are sent.
113
+ * @returns {Promise<void>}
114
+ */
115
+ async function shutdown() {
116
+ if (!enabled || !client) return;
117
+
118
+ try {
119
+ await client.shutdown();
120
+ } catch {
121
+ // Best-effort flush
122
+ } finally {
123
+ client = null;
124
+ enabled = false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if telemetry is currently active
130
+ * @returns {boolean}
131
+ */
132
+ function isEnabled() {
133
+ return enabled;
134
+ }
135
+
136
+ module.exports = {
137
+ init,
138
+ capture,
139
+ captureError,
140
+ shutdown,
141
+ isEnabled,
142
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Telemetry configuration management
3
+ *
4
+ * Stores telemetry preferences in ~/.git-watchtower/config.json
5
+ * (separate from per-project .watchtowerrc.json).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+ const os = require('os');
12
+
13
+ const CONFIG_DIR_NAME = '.git-watchtower';
14
+ const CONFIG_FILE_NAME = 'config.json';
15
+
16
+ /**
17
+ * Get the telemetry config directory path
18
+ * @returns {string}
19
+ */
20
+ function getConfigDir() {
21
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
22
+ }
23
+
24
+ /**
25
+ * Get the telemetry config file path
26
+ * @returns {string}
27
+ */
28
+ function getConfigPath() {
29
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
30
+ }
31
+
32
+ /**
33
+ * Load telemetry config from disk
34
+ * @returns {{ telemetryEnabled: boolean, distinctId: string, promptedAt: string } | null}
35
+ */
36
+ function loadTelemetryConfig() {
37
+ try {
38
+ const data = fs.readFileSync(getConfigPath(), 'utf8');
39
+ const config = JSON.parse(data);
40
+ if (config && typeof config === 'object') {
41
+ return config;
42
+ }
43
+ return null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Save telemetry config to disk
51
+ * @param {{ telemetryEnabled: boolean, distinctId: string, promptedAt: string }} config
52
+ */
53
+ function saveTelemetryConfig(config) {
54
+ const dir = getConfigDir();
55
+ if (!fs.existsSync(dir)) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ }
58
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf8');
59
+ }
60
+
61
+ /**
62
+ * Get existing distinctId or create a new one
63
+ * @returns {string}
64
+ */
65
+ function getOrCreateDistinctId() {
66
+ const config = loadTelemetryConfig();
67
+ if (config && config.distinctId) {
68
+ return config.distinctId;
69
+ }
70
+ return crypto.randomUUID();
71
+ }
72
+
73
+ /**
74
+ * Check if telemetry is enabled
75
+ *
76
+ * Priority:
77
+ * 1. GIT_WATCHTOWER_TELEMETRY=false env var always disables (CI/corporate)
78
+ * 2. If no config file exists, telemetry is disabled (not yet prompted)
79
+ * 3. Otherwise, return the stored preference
80
+ *
81
+ * @returns {boolean}
82
+ */
83
+ function isTelemetryEnabled() {
84
+ const envVar = process.env.GIT_WATCHTOWER_TELEMETRY;
85
+ if (envVar !== undefined && envVar.toLowerCase() === 'false') {
86
+ return false;
87
+ }
88
+
89
+ const config = loadTelemetryConfig();
90
+ if (!config) {
91
+ return false;
92
+ }
93
+
94
+ return config.telemetryEnabled === true;
95
+ }
96
+
97
+ /**
98
+ * Check if the user has already been prompted for telemetry
99
+ * @returns {boolean}
100
+ */
101
+ function hasBeenPrompted() {
102
+ return loadTelemetryConfig() !== null;
103
+ }
104
+
105
+ /**
106
+ * Check if telemetry is force-disabled via environment variable
107
+ * @returns {boolean}
108
+ */
109
+ function isEnvDisabled() {
110
+ const envVar = process.env.GIT_WATCHTOWER_TELEMETRY;
111
+ return envVar !== undefined && envVar.toLowerCase() === 'false';
112
+ }
113
+
114
+ module.exports = {
115
+ getConfigDir,
116
+ getConfigPath,
117
+ loadTelemetryConfig,
118
+ saveTelemetryConfig,
119
+ getOrCreateDistinctId,
120
+ isTelemetryEnabled,
121
+ hasBeenPrompted,
122
+ isEnvDisabled,
123
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Telemetry public API
3
+ *
4
+ * Usage:
5
+ * const telemetry = require('../src/telemetry');
6
+ * await telemetry.promptIfNeeded(promptYesNoFn);
7
+ * telemetry.init({ version: '1.6.1' });
8
+ * telemetry.capture('tool_launched', { os: 'linux' });
9
+ * await telemetry.shutdown();
10
+ */
11
+
12
+ const analytics = require('./analytics');
13
+ const config = require('./config');
14
+
15
+ /**
16
+ * Show opt-in telemetry prompt if the user hasn't been asked yet.
17
+ *
18
+ * Skips the prompt when:
19
+ * - Already prompted (config file exists)
20
+ * - Env var forces telemetry off
21
+ * - No TTY (non-interactive / CI)
22
+ *
23
+ * @param {(question: string, defaultValue?: boolean) => Promise<boolean>} promptYesNo
24
+ */
25
+ async function promptIfNeeded(promptYesNo) {
26
+ // Already prompted — respect existing choice
27
+ if (config.hasBeenPrompted()) {
28
+ return;
29
+ }
30
+
31
+ // Env var forces off — no point asking
32
+ if (config.isEnvDisabled()) {
33
+ return;
34
+ }
35
+
36
+ // Non-interactive — default to disabled
37
+ if (!process.stdin.isTTY) {
38
+ return;
39
+ }
40
+
41
+ console.log('');
42
+ console.log('\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510');
43
+ console.log('\u2502 Help improve Git Watchtower \u2502');
44
+ console.log('\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524');
45
+ console.log('\u2502 We\'d love to collect anonymous usage data to improve \u2502');
46
+ console.log('\u2502 Git Watchtower. This telemetry is used to improve the \u2502');
47
+ console.log('\u2502 product. It includes: \u2502');
48
+ console.log('\u2502 - Which features are used (not your code or data) \u2502');
49
+ console.log('\u2502 - Error types encountered (not stack traces) \u2502');
50
+ console.log('\u2502 - Session duration and OS/Node.js version \u2502');
51
+ console.log('\u2502 \u2502');
52
+ console.log('\u2502 No personal information, file contents, branch names, \u2502');
53
+ console.log('\u2502 or repository data is ever collected. \u2502');
54
+ console.log('\u2502 \u2502');
55
+ console.log('\u2502 You can change this anytime by editing: \u2502');
56
+ console.log('\u2502 ~/.git-watchtower/config.json \u2502');
57
+ console.log('\u2502 Or set GIT_WATCHTOWER_TELEMETRY=false \u2502');
58
+ console.log('\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518');
59
+ console.log('');
60
+
61
+ const answer = await promptYesNo('Enable anonymous telemetry to help improve Git Watchtower?', false);
62
+
63
+ const distinctId = config.getOrCreateDistinctId();
64
+ config.saveTelemetryConfig({
65
+ telemetryEnabled: answer,
66
+ distinctId,
67
+ promptedAt: new Date().toISOString(),
68
+ });
69
+
70
+ if (answer) {
71
+ console.log(' Thank you! Telemetry enabled.\n');
72
+ } else {
73
+ console.log(' No problem! Telemetry disabled.\n');
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ // Analytics
79
+ init: analytics.init,
80
+ capture: analytics.capture,
81
+ captureError: analytics.captureError,
82
+ shutdown: analytics.shutdown,
83
+ isEnabled: analytics.isEnabled,
84
+
85
+ // Prompt
86
+ promptIfNeeded,
87
+
88
+ // Config (for advanced use)
89
+ isTelemetryEnabled: config.isTelemetryEnabled,
90
+ hasBeenPrompted: config.hasBeenPrompted,
91
+ loadTelemetryConfig: config.loadTelemetryConfig,
92
+ saveTelemetryConfig: config.saveTelemetryConfig,
93
+ };