@saurabhreo/package-tracker 1.0.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.
- package/README.md +242 -0
- package/index.js +29 -0
- package/package.json +39 -0
- package/src/collector.js +160 -0
- package/src/config.js +132 -0
- package/src/sender.js +143 -0
- package/src/tracker.js +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Package Tracker
|
|
2
|
+
|
|
3
|
+
A lightweight npm package tracking utility similar to Reo. Tracks package installations and sends analytics data to your backend service.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Zero Runtime Overhead** - Only runs during installation, never at runtime
|
|
8
|
+
- 🔒 **Privacy-First** - Respects opt-out mechanisms and user preferences
|
|
9
|
+
- ⚡ **Non-Blocking** - Never interrupts package installation
|
|
10
|
+
- 📊 **Rich Data Collection** - System info, OS stats, package metadata, and more
|
|
11
|
+
- 🔄 **Retry Logic** - Automatic retries with exponential backoff
|
|
12
|
+
- 🎛️ **Configurable** - Environment variables and package.json settings
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this package as a dependency to any npm package you want to track:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install --save @package-tracker/tracker
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Once installed, the tracker will automatically run after `npm install` completes.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
### Environment Variables
|
|
27
|
+
|
|
28
|
+
- `PACKAGE_TRACKER_ANALYTICS=false` - Disable tracking (opt-out)
|
|
29
|
+
- `PACKAGE_TRACKER_ANALYTICS=true` - Enable tracking (for opt-in mode)
|
|
30
|
+
- `PACKAGE_TRACKER_VERBOSE=true` - Enable verbose logging
|
|
31
|
+
- `PACKAGE_TRACKER_ENDPOINT=https://telemetry.reo.dev/data` - Set tracking endpoint URL
|
|
32
|
+
|
|
33
|
+
### Package.json Settings
|
|
34
|
+
|
|
35
|
+
Add configuration to your `package.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"reoSettings": {
|
|
40
|
+
"defaultOptIn": false,
|
|
41
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Settings
|
|
47
|
+
|
|
48
|
+
- `defaultOptIn` (boolean, default: `true`)
|
|
49
|
+
- `true`: Tracking enabled by default (opt-out mode)
|
|
50
|
+
- `false`: Tracking disabled by default (opt-in mode)
|
|
51
|
+
|
|
52
|
+
- `endpoint` (string, optional)
|
|
53
|
+
- Custom endpoint URL for tracking data
|
|
54
|
+
- Falls back to `PACKAGE_TRACKER_ENDPOINT` environment variable
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
1. **Postinstall Hook**: The tracker runs automatically via npm's `postinstall` script
|
|
59
|
+
2. **Data Collection**: Gathers system info, package metadata, and installation context
|
|
60
|
+
3. **Telemetry Sending**: Sends data to your configured endpoint via HTTP POST
|
|
61
|
+
4. **Non-Blocking**: All operations are asynchronous and won't block installations
|
|
62
|
+
|
|
63
|
+
## Data Collected
|
|
64
|
+
|
|
65
|
+
The tracker collects the following information:
|
|
66
|
+
|
|
67
|
+
### Package Information
|
|
68
|
+
- Package name and version
|
|
69
|
+
- Installation type (dependency, global, top-level)
|
|
70
|
+
|
|
71
|
+
### System Information
|
|
72
|
+
- Platform (darwin, linux, win32, etc.)
|
|
73
|
+
- Architecture (x64, arm64, etc.)
|
|
74
|
+
- Node.js version
|
|
75
|
+
- OS type and release
|
|
76
|
+
- CPU count
|
|
77
|
+
|
|
78
|
+
### Installation Context
|
|
79
|
+
- Timestamp
|
|
80
|
+
- Current working directory
|
|
81
|
+
- npm version
|
|
82
|
+
- Node environment
|
|
83
|
+
|
|
84
|
+
### Optional Metadata
|
|
85
|
+
- Git user information (if available)
|
|
86
|
+
- Email domain (for company detection)
|
|
87
|
+
- CI/CD environment detection
|
|
88
|
+
|
|
89
|
+
## Privacy & Opt-Out
|
|
90
|
+
|
|
91
|
+
### For Package Authors
|
|
92
|
+
|
|
93
|
+
To make tracking opt-in (disabled by default):
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"reoSettings": {
|
|
98
|
+
"defaultOptIn": false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### For End Users
|
|
104
|
+
|
|
105
|
+
Users can opt-out by setting:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export PACKAGE_TRACKER_ANALYTICS=false
|
|
109
|
+
npm install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or on Windows:
|
|
113
|
+
|
|
114
|
+
```cmd
|
|
115
|
+
set PACKAGE_TRACKER_ANALYTICS=false
|
|
116
|
+
npm install
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Integration Example
|
|
120
|
+
|
|
121
|
+
### Basic Integration
|
|
122
|
+
|
|
123
|
+
1. Add the tracker to your package:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install --save @package-tracker/tracker
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
2. Configure your endpoint in `package.json`:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"reoSettings": {
|
|
134
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
3. Publish your package - tracking will work automatically!
|
|
140
|
+
|
|
141
|
+
### Advanced Configuration
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"name": "my-package",
|
|
146
|
+
"version": "1.0.0",
|
|
147
|
+
"reoSettings": {
|
|
148
|
+
"defaultOptIn": false,
|
|
149
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
150
|
+
},
|
|
151
|
+
"dependencies": {
|
|
152
|
+
"@package-tracker/tracker": "^1.0.0"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Backend API Requirements
|
|
158
|
+
|
|
159
|
+
Your backend endpoint should:
|
|
160
|
+
|
|
161
|
+
1. Accept POST requests with JSON payload
|
|
162
|
+
2. Return 2xx status codes for success
|
|
163
|
+
3. Handle the following payload structure:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"package": {
|
|
168
|
+
"name": "package-name",
|
|
169
|
+
"version": "1.0.0"
|
|
170
|
+
},
|
|
171
|
+
"system": {
|
|
172
|
+
"platform": "darwin",
|
|
173
|
+
"arch": "x64",
|
|
174
|
+
"nodeVersion": "v18.0.0",
|
|
175
|
+
"osType": "Darwin",
|
|
176
|
+
"osRelease": "21.0.0",
|
|
177
|
+
"osPlatform": "darwin",
|
|
178
|
+
"cpuCount": 8
|
|
179
|
+
},
|
|
180
|
+
"installation": {
|
|
181
|
+
"timestamp": "2026-02-08T12:00:00.000Z",
|
|
182
|
+
"cwd": "/path/to/project",
|
|
183
|
+
"isTopLevel": false,
|
|
184
|
+
"npmVersion": "9.0.0",
|
|
185
|
+
"nodeEnv": "production"
|
|
186
|
+
},
|
|
187
|
+
"metadata": {
|
|
188
|
+
"emailDomain": "example.com",
|
|
189
|
+
"ci": [{"GITHUB_ACTIONS": "GitHub Actions"}]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
### Testing
|
|
197
|
+
|
|
198
|
+
See [TESTING.md](./TESTING.md) for comprehensive testing guide.
|
|
199
|
+
|
|
200
|
+
**Quick Test:**
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Run test suite
|
|
204
|
+
npm test
|
|
205
|
+
|
|
206
|
+
# Start mock server for testing
|
|
207
|
+
npm run test:server
|
|
208
|
+
|
|
209
|
+
# Test directly with verbose output
|
|
210
|
+
PACKAGE_TRACKER_VERBOSE=true \
|
|
211
|
+
PACKAGE_TRACKER_ENDPOINT=https://httpbin.org/post \
|
|
212
|
+
node index.js
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Testing Locally
|
|
216
|
+
|
|
217
|
+
1. Set verbose mode:
|
|
218
|
+
```bash
|
|
219
|
+
export PACKAGE_TRACKER_VERBOSE=true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
2. Test with a mock endpoint or your backend:
|
|
223
|
+
```bash
|
|
224
|
+
export PACKAGE_TRACKER_ENDPOINT=https://telemetry.reo.dev/data
|
|
225
|
+
npm install
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Debugging
|
|
229
|
+
|
|
230
|
+
Enable verbose logging to see what data is being collected and sent:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
PACKAGE_TRACKER_VERBOSE=true npm install
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
239
|
+
|
|
240
|
+
## Similar Projects
|
|
241
|
+
|
|
242
|
+
This package is inspired by [@reo/reo](https://www.npmjs.com/package/@reo/reo) and provides similar functionality for custom backend services.
|
package/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entry point for package tracker
|
|
5
|
+
* This file is executed when the postinstall script runs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Tracker = require('./src/tracker');
|
|
9
|
+
|
|
10
|
+
// Run tracker asynchronously with a maximum timeout
|
|
11
|
+
// This ensures it never blocks the installation process
|
|
12
|
+
const tracker = new Tracker();
|
|
13
|
+
|
|
14
|
+
// Maximum time to wait before exiting (10 seconds)
|
|
15
|
+
const MAX_WAIT_TIME = 10000;
|
|
16
|
+
|
|
17
|
+
// Execute tracking with timeout protection
|
|
18
|
+
const trackingPromise = tracker.track().catch(() => {
|
|
19
|
+
// Silently handle any errors - never break installations
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Race between tracking completion and maximum wait time
|
|
23
|
+
Promise.race([
|
|
24
|
+
trackingPromise,
|
|
25
|
+
new Promise((resolve) => setTimeout(resolve, MAX_WAIT_TIME)),
|
|
26
|
+
]).finally(() => {
|
|
27
|
+
// Exit after tracking completes or timeout, ensuring we don't hang
|
|
28
|
+
process.exit(0);
|
|
29
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saurabhreo/package-tracker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Package tracking utility similar to Reo - tracks npm package installations",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"postinstall": "node index.js",
|
|
8
|
+
"test": "node test.js",
|
|
9
|
+
"test:server": "node test-mock-server.js",
|
|
10
|
+
"prepublishOnly": "npm test",
|
|
11
|
+
"publish:patch": "npm version patch && npm publish --access public",
|
|
12
|
+
"publish:minor": "npm version minor && npm publish --access public",
|
|
13
|
+
"publish:major": "npm version major && npm publish --access public"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"analytics",
|
|
17
|
+
"tracking",
|
|
18
|
+
"npm",
|
|
19
|
+
"package-analytics",
|
|
20
|
+
"telemetry"
|
|
21
|
+
],
|
|
22
|
+
"author": "Saurabh <saurabh@reo.dev>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=12.0.0"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/reodotdev/package_tracker.git"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.js",
|
|
33
|
+
"src/**/*",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/collector.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Data collector for package tracking
|
|
7
|
+
* Gathers system information, package metadata, and installation context
|
|
8
|
+
*/
|
|
9
|
+
class Collector {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Collect all tracking data
|
|
16
|
+
*/
|
|
17
|
+
collect() {
|
|
18
|
+
try {
|
|
19
|
+
const data = {
|
|
20
|
+
// Package information
|
|
21
|
+
package: {
|
|
22
|
+
name: this.config.getPackageName(),
|
|
23
|
+
version: this.config.getPackageVersion(),
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// System information
|
|
27
|
+
system: {
|
|
28
|
+
platform: process.platform,
|
|
29
|
+
arch: process.arch,
|
|
30
|
+
nodeVersion: process.version,
|
|
31
|
+
osType: os.type(),
|
|
32
|
+
osRelease: os.release(),
|
|
33
|
+
osPlatform: os.platform(),
|
|
34
|
+
cpuCount: os.cpus().length,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Installation context
|
|
38
|
+
installation: {
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
isTopLevel: this.config.isTopLevelInstall(),
|
|
42
|
+
npmVersion: process.env.npm_version || 'unknown',
|
|
43
|
+
nodeEnv: process.env.NODE_ENV || 'unknown',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Optional: Company/user information
|
|
47
|
+
metadata: this.collectMetadata(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Return minimal data if collection fails
|
|
53
|
+
return {
|
|
54
|
+
package: {
|
|
55
|
+
name: this.config.getPackageName(),
|
|
56
|
+
version: this.config.getPackageVersion(),
|
|
57
|
+
},
|
|
58
|
+
error: 'Collection failed',
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collect optional metadata (company info, git info, etc.)
|
|
66
|
+
*/
|
|
67
|
+
collectMetadata() {
|
|
68
|
+
const metadata = {};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Try to get git user info
|
|
72
|
+
const gitConfig = this.getGitConfig();
|
|
73
|
+
if (gitConfig.user) {
|
|
74
|
+
metadata.gitUser = gitConfig.user;
|
|
75
|
+
}
|
|
76
|
+
if (gitConfig.email) {
|
|
77
|
+
// Only include domain, not full email for privacy
|
|
78
|
+
const emailDomain = gitConfig.email.split('@')[1];
|
|
79
|
+
if (emailDomain) {
|
|
80
|
+
metadata.emailDomain = emailDomain;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Try to detect CI/CD environment
|
|
85
|
+
const ciInfo = this.detectCI();
|
|
86
|
+
if (ciInfo) {
|
|
87
|
+
metadata.ci = ciInfo;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to get npm user info (if available)
|
|
91
|
+
const npmUser = process.env.npm_config_user;
|
|
92
|
+
if (npmUser) {
|
|
93
|
+
metadata.npmUser = npmUser;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Silently fail - metadata is optional
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return metadata;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read git config to get user information
|
|
104
|
+
*/
|
|
105
|
+
getGitConfig() {
|
|
106
|
+
const gitConfig = {};
|
|
107
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
111
|
+
const content = fs.readFileSync(gitConfigPath, 'utf8');
|
|
112
|
+
|
|
113
|
+
// Simple parsing of git config
|
|
114
|
+
const userMatch = content.match(/\[user\]\s+name\s*=\s*(.+)/i);
|
|
115
|
+
const emailMatch = content.match(/\[user\]\s+email\s*=\s*(.+)/i);
|
|
116
|
+
|
|
117
|
+
if (userMatch) {
|
|
118
|
+
gitConfig.user = userMatch[1].trim();
|
|
119
|
+
}
|
|
120
|
+
if (emailMatch) {
|
|
121
|
+
gitConfig.email = emailMatch[1].trim();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Silently fail
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return gitConfig;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect CI/CD environment
|
|
133
|
+
*/
|
|
134
|
+
detectCI() {
|
|
135
|
+
const ciEnvs = {
|
|
136
|
+
CI: process.env.CI,
|
|
137
|
+
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS ? 'GitHub Actions' : null,
|
|
138
|
+
GITLAB_CI: process.env.GITLAB_CI ? 'GitLab CI' : null,
|
|
139
|
+
JENKINS: process.env.JENKINS_URL ? 'Jenkins' : null,
|
|
140
|
+
TRAVIS: process.env.TRAVIS ? 'Travis CI' : null,
|
|
141
|
+
CIRCLE_CI: process.env.CIRCLE_CI ? 'CircleCI' : null,
|
|
142
|
+
BITBUCKET_BUILD_NUMBER: process.env.BITBUCKET_BUILD_NUMBER ? 'Bitbucket Pipelines' : null,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const detected = Object.entries(ciEnvs)
|
|
146
|
+
.filter(([_, value]) => value !== null && value !== undefined)
|
|
147
|
+
.map(([key, value]) => ({ [key]: value }));
|
|
148
|
+
|
|
149
|
+
return detected.length > 0 ? detected : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format data for sending (can be overridden for custom formatting)
|
|
154
|
+
*/
|
|
155
|
+
format(data) {
|
|
156
|
+
return JSON.stringify(data);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = Collector;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration handler for package tracker
|
|
6
|
+
* Reads environment variables and package.json settings
|
|
7
|
+
*/
|
|
8
|
+
class Config {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.packageJsonPath = this.findPackageJson();
|
|
11
|
+
this.packageJson = this.loadPackageJson();
|
|
12
|
+
this.reoSettings = this.packageJson?.reoSettings || {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find package.json by walking up the directory tree
|
|
17
|
+
*/
|
|
18
|
+
findPackageJson() {
|
|
19
|
+
let currentDir = process.cwd();
|
|
20
|
+
const root = path.parse(currentDir).root;
|
|
21
|
+
|
|
22
|
+
while (currentDir !== root) {
|
|
23
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
24
|
+
if (fs.existsSync(packagePath)) {
|
|
25
|
+
return packagePath;
|
|
26
|
+
}
|
|
27
|
+
currentDir = path.dirname(currentDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback to current directory
|
|
31
|
+
return path.join(process.cwd(), 'package.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load package.json content
|
|
36
|
+
*/
|
|
37
|
+
loadPackageJson() {
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(this.packageJsonPath)) {
|
|
40
|
+
const content = fs.readFileSync(this.packageJsonPath, 'utf8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Silently fail - package.json might not exist or be invalid
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if tracking is enabled
|
|
51
|
+
* Respects environment variable and package.json settings
|
|
52
|
+
*/
|
|
53
|
+
isTrackingEnabled() {
|
|
54
|
+
// Check environment variable first (highest priority)
|
|
55
|
+
const envOptOut = process.env.PACKAGE_TRACKER_ANALYTICS === 'false';
|
|
56
|
+
if (envOptOut) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if defaultOptIn is set to false in package.json
|
|
61
|
+
if (this.reoSettings.defaultOptIn === false) {
|
|
62
|
+
// In opt-in mode, check if explicitly enabled
|
|
63
|
+
return process.env.PACKAGE_TRACKER_ANALYTICS === 'true';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Default: opt-out mode (enabled by default)
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if verbose logging is enabled
|
|
72
|
+
*/
|
|
73
|
+
isVerbose() {
|
|
74
|
+
return process.env.PACKAGE_TRACKER_VERBOSE === 'true';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get tracking endpoint URL
|
|
79
|
+
* Can be configured via environment variable or package.json
|
|
80
|
+
*/
|
|
81
|
+
getEndpointUrl() {
|
|
82
|
+
return (
|
|
83
|
+
process.env.PACKAGE_TRACKER_ENDPOINT ||
|
|
84
|
+
this.reoSettings.endpoint ||
|
|
85
|
+
'https://telemetry.reo.dev/data' // Default endpoint
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get package name from package.json
|
|
91
|
+
*/
|
|
92
|
+
getPackageName() {
|
|
93
|
+
return this.packageJson?.name || 'unknown';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get package version from package.json
|
|
98
|
+
*/
|
|
99
|
+
getPackageVersion() {
|
|
100
|
+
return this.packageJson?.version || 'unknown';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if this is a top-level installation
|
|
105
|
+
* Reo only tracks when installed as dependency or globally
|
|
106
|
+
*/
|
|
107
|
+
isTopLevelInstall() {
|
|
108
|
+
// Check if we're in node_modules (dependency) or global install
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const isInNodeModules = cwd.includes('node_modules');
|
|
111
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
112
|
+
|
|
113
|
+
// If not in node_modules and not global, it's likely top-level
|
|
114
|
+
return !isInNodeModules && !isGlobal;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all configuration for logging
|
|
119
|
+
*/
|
|
120
|
+
getConfig() {
|
|
121
|
+
return {
|
|
122
|
+
enabled: this.isTrackingEnabled(),
|
|
123
|
+
verbose: this.isVerbose(),
|
|
124
|
+
endpoint: this.getEndpointUrl(),
|
|
125
|
+
packageName: this.getPackageName(),
|
|
126
|
+
packageVersion: this.getPackageVersion(),
|
|
127
|
+
isTopLevel: this.isTopLevelInstall(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = Config;
|
package/src/sender.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP client for sending tracking data
|
|
6
|
+
* Implements retry logic, timeouts, and error handling
|
|
7
|
+
*/
|
|
8
|
+
class Sender {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.endpoint = config.getEndpointUrl();
|
|
12
|
+
this.maxRetries = 3;
|
|
13
|
+
this.timeout = 5000; // 5 seconds per request
|
|
14
|
+
this.maxTotalTimeout = 10000; // 10 seconds total maximum (including retries)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Send tracking data to the endpoint
|
|
19
|
+
* Non-blocking and best-effort (never throws)
|
|
20
|
+
*/
|
|
21
|
+
async send(data) {
|
|
22
|
+
if (!this.config.isTrackingEnabled()) {
|
|
23
|
+
if (this.config.isVerbose()) {
|
|
24
|
+
console.log('[package-tracker] Tracking disabled');
|
|
25
|
+
}
|
|
26
|
+
return { success: false, reason: 'disabled' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Don't track top-level installations by default (like Reo)
|
|
30
|
+
if (this.config.isTopLevelInstall()) {
|
|
31
|
+
if (this.config.isVerbose()) {
|
|
32
|
+
console.log('[package-tracker] Skipping top-level installation');
|
|
33
|
+
}
|
|
34
|
+
return { success: false, reason: 'top-level-install' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
38
|
+
|
|
39
|
+
if (this.config.isVerbose()) {
|
|
40
|
+
console.log('[package-tracker] Sending data:', payload);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Enforce maximum total timeout to prevent hanging
|
|
45
|
+
const timeoutPromise = this.sleep(this.maxTotalTimeout).then(() => {
|
|
46
|
+
throw new Error('Maximum timeout exceeded');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result = await Promise.race([
|
|
50
|
+
this.sendWithRetry(payload),
|
|
51
|
+
timeoutPromise,
|
|
52
|
+
]);
|
|
53
|
+
return result;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Never throw - this is best-effort
|
|
56
|
+
if (this.config.isVerbose()) {
|
|
57
|
+
console.error('[package-tracker] Failed to send:', error.message);
|
|
58
|
+
}
|
|
59
|
+
return { success: false, error: error.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Send with retry logic
|
|
65
|
+
*/
|
|
66
|
+
async sendWithRetry(payload, retryCount = 0) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await this.makeRequest(payload);
|
|
69
|
+
return { success: true, statusCode: result.statusCode };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (retryCount < this.maxRetries) {
|
|
72
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
73
|
+
const delay = Math.pow(2, retryCount) * 1000;
|
|
74
|
+
await this.sleep(delay);
|
|
75
|
+
return this.sendWithRetry(payload, retryCount + 1);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Make HTTP/HTTPS request
|
|
83
|
+
*/
|
|
84
|
+
makeRequest(payload) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const url = new URL(this.endpoint);
|
|
87
|
+
const isHttps = url.protocol === 'https:';
|
|
88
|
+
const client = isHttps ? https : http;
|
|
89
|
+
|
|
90
|
+
const options = {
|
|
91
|
+
hostname: url.hostname,
|
|
92
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
93
|
+
path: url.pathname + url.search,
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
98
|
+
'User-Agent': `package-tracker/${this.config.getPackageVersion()}`,
|
|
99
|
+
},
|
|
100
|
+
timeout: this.timeout,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const req = client.request(options, (res) => {
|
|
104
|
+
let data = '';
|
|
105
|
+
|
|
106
|
+
res.on('data', (chunk) => {
|
|
107
|
+
data += chunk;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
res.on('end', () => {
|
|
111
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
112
|
+
resolve({ statusCode: res.statusCode, data });
|
|
113
|
+
} else {
|
|
114
|
+
reject(
|
|
115
|
+
new Error(`Request failed with status ${res.statusCode}`)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
req.on('error', (error) => {
|
|
122
|
+
reject(error);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
req.on('timeout', () => {
|
|
126
|
+
req.destroy();
|
|
127
|
+
reject(new Error('Request timeout'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
req.write(payload);
|
|
131
|
+
req.end();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sleep utility for retry delays
|
|
137
|
+
*/
|
|
138
|
+
sleep(ms) {
|
|
139
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = Sender;
|
package/src/tracker.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const Config = require('./config');
|
|
2
|
+
const Collector = require('./collector');
|
|
3
|
+
const Sender = require('./sender');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Main tracker orchestrator
|
|
7
|
+
* Coordinates configuration, data collection, and sending
|
|
8
|
+
*/
|
|
9
|
+
class Tracker {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.config = new Config();
|
|
12
|
+
this.collector = new Collector(this.config);
|
|
13
|
+
this.sender = new Sender(this.config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the tracking process
|
|
18
|
+
* This is the main entry point
|
|
19
|
+
*/
|
|
20
|
+
async track() {
|
|
21
|
+
try {
|
|
22
|
+
// Log tracking status if verbose
|
|
23
|
+
if (this.config.isVerbose()) {
|
|
24
|
+
const config = this.config.getConfig();
|
|
25
|
+
console.log('[package-tracker] Configuration:', JSON.stringify(config, null, 2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if tracking is enabled
|
|
29
|
+
if (!this.config.isTrackingEnabled()) {
|
|
30
|
+
if (this.config.isVerbose()) {
|
|
31
|
+
console.log('[package-tracker] Tracking is disabled');
|
|
32
|
+
}
|
|
33
|
+
return { success: false, reason: 'disabled' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Collect data
|
|
37
|
+
const data = this.collector.collect();
|
|
38
|
+
|
|
39
|
+
if (this.config.isVerbose()) {
|
|
40
|
+
console.log('[package-tracker] Collected data:', JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Send data (non-blocking, best-effort)
|
|
44
|
+
const result = await this.sender.send(data);
|
|
45
|
+
|
|
46
|
+
// Log result if verbose
|
|
47
|
+
if (this.config.isVerbose()) {
|
|
48
|
+
console.log('[package-tracker] Send result:', result);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Never throw - this should never break installations
|
|
54
|
+
if (this.config.isVerbose()) {
|
|
55
|
+
console.error('[package-tracker] Error:', error.message);
|
|
56
|
+
}
|
|
57
|
+
return { success: false, error: error.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current configuration (for debugging)
|
|
63
|
+
*/
|
|
64
|
+
getConfig() {
|
|
65
|
+
return this.config.getConfig();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = Tracker;
|