@saurabhreo/reo-census 1.2.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 +244 -0
- package/index.js +29 -0
- package/package.json +39 -0
- package/src/collector.js +299 -0
- package/src/config.js +194 -0
- package/src/sender.js +143 -0
- package/src/tracker.js +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
- 🔄 **Retry Logic** - Automatic retries with exponential backoff
|
|
11
|
+
- 🎛️ **Configurable** - Environment variables and package.json settings
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add this package as a dependency to any npm package you want to track:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save @saurabhreo/reo-census
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Once installed, the tracker will automatically run after `npm install` completes.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### Environment Variables
|
|
26
|
+
|
|
27
|
+
- `PACKAGE_TRACKER_ANALYTICS=false` - Disable tracking (opt-out)
|
|
28
|
+
- `PACKAGE_TRACKER_ANALYTICS=true` - Enable tracking (for opt-in mode)
|
|
29
|
+
- `PACKAGE_TRACKER_VERBOSE=true` - Enable verbose logging
|
|
30
|
+
- `PACKAGE_TRACKER_ENDPOINT=https://telemetry.reo.dev/data` - Set tracking endpoint URL
|
|
31
|
+
- `PACKAGE_TRACKER_DATA_MODE=partial` - Data collection mode (default: `partial`)
|
|
32
|
+
- `partial`: Sends basic metadata (email domain only, no full email, no dependencies)
|
|
33
|
+
- `full`: Includes full email and dependency information
|
|
34
|
+
|
|
35
|
+
### Package.json Settings
|
|
36
|
+
|
|
37
|
+
Add configuration to your `package.json` under `reoSettings`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"reoSettings": {
|
|
42
|
+
"defaultOptIn": false,
|
|
43
|
+
"endpoint": "https://telemetry.reo.dev/data",
|
|
44
|
+
"dataMode": "full"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Settings
|
|
50
|
+
|
|
51
|
+
- `defaultOptIn` (boolean, default: `true`)
|
|
52
|
+
- `true`: Tracking enabled by default (opt-out mode)
|
|
53
|
+
- `false`: Tracking disabled by default (opt-in mode)
|
|
54
|
+
|
|
55
|
+
- `endpoint` (string, optional)
|
|
56
|
+
- Custom endpoint URL for tracking data
|
|
57
|
+
- Falls back to `PACKAGE_TRACKER_ENDPOINT` environment variable
|
|
58
|
+
|
|
59
|
+
- `dataMode` (string, default: `"partial"`)
|
|
60
|
+
- `"partial"`: Email domain only, no full email, no dependencies
|
|
61
|
+
- `"full"`: Full email and dependency list included
|
|
62
|
+
- **Note:** `PACKAGE_TRACKER_DATA_MODE` environment variable takes priority over this setting
|
|
63
|
+
|
|
64
|
+
## Data Collection Modes
|
|
65
|
+
|
|
66
|
+
The tracker supports two data collection modes controlled by `PACKAGE_TRACKER_DATA_MODE`:
|
|
67
|
+
|
|
68
|
+
### Partial Mode (Default)
|
|
69
|
+
|
|
70
|
+
**Default behavior** - Privacy-focused data collection:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export PACKAGE_TRACKER_DATA_MODE=partial
|
|
74
|
+
# or omit the variable (partial is default)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**What's collected:**
|
|
78
|
+
- ✅ Package name and version
|
|
79
|
+
- ✅ System information (platform, arch, Node version, OS)
|
|
80
|
+
- ✅ Installation context (timestamp, cwd, npm version)
|
|
81
|
+
- ✅ Git username (if available)
|
|
82
|
+
- ✅ Email domain only (e.g., `reo.dev`) - **not full email**
|
|
83
|
+
- ✅ CI/CD detection
|
|
84
|
+
- ❌ Full email address
|
|
85
|
+
- ❌ Dependency list (other packages used)
|
|
86
|
+
|
|
87
|
+
### Full Mode
|
|
88
|
+
|
|
89
|
+
**Enhanced data collection** - Includes additional information:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export PACKAGE_TRACKER_DATA_MODE=full
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**What's collected:**
|
|
96
|
+
- ✅ Everything from partial mode, plus:
|
|
97
|
+
- ✅ Full email address (from git config)
|
|
98
|
+
- ✅ Dependency list (dependencies, devDependencies, peerDependencies, optionalDependencies)
|
|
99
|
+
|
|
100
|
+
**Example payload in full mode:**
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"package": {
|
|
105
|
+
"name": "my-package",
|
|
106
|
+
"version": "1.0.0",
|
|
107
|
+
"dependencies": {
|
|
108
|
+
"dependencies": {
|
|
109
|
+
"express": "^4.18.0",
|
|
110
|
+
"lodash": "^4.17.21"
|
|
111
|
+
},
|
|
112
|
+
"devDependencies": {
|
|
113
|
+
"jest": "^29.0.0"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"metadata": {
|
|
118
|
+
"gitUser": "John Doe",
|
|
119
|
+
"email": "john@example.com",
|
|
120
|
+
"emailDomain": "example.com"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Privacy Note:** Use `full` mode only when you need detailed analytics and have user consent. The default `partial` mode protects user privacy by not collecting sensitive information.
|
|
126
|
+
|
|
127
|
+
## Privacy & Opt-Out
|
|
128
|
+
|
|
129
|
+
### For Package Authors
|
|
130
|
+
|
|
131
|
+
To make tracking opt-in (disabled by default):
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"reoSettings": {
|
|
136
|
+
"defaultOptIn": false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### For End Users
|
|
142
|
+
|
|
143
|
+
Users can opt-out by setting:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
export PACKAGE_TRACKER_ANALYTICS=false
|
|
147
|
+
npm install
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Or on Windows:
|
|
151
|
+
|
|
152
|
+
```cmd
|
|
153
|
+
set PACKAGE_TRACKER_ANALYTICS=false
|
|
154
|
+
npm install
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Integration Example
|
|
158
|
+
|
|
159
|
+
### Basic Integration
|
|
160
|
+
|
|
161
|
+
1. Add the tracker to your package:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm install --save @saurabhreo/reo-census
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. Configure your endpoint in `package.json`:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"reoSettings": {
|
|
172
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. Publish your package - tracking will work automatically!
|
|
178
|
+
|
|
179
|
+
### Advanced Configuration
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"name": "my-package",
|
|
184
|
+
"version": "1.0.0",
|
|
185
|
+
"reoSettings": {
|
|
186
|
+
"defaultOptIn": false,
|
|
187
|
+
"endpoint": "https://telemetry.reo.dev/data",
|
|
188
|
+
"dataMode": "full"
|
|
189
|
+
},
|
|
190
|
+
"dependencies": {
|
|
191
|
+
"@saurabhreo/reo-census": "^1.1.0"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
### Testing
|
|
199
|
+
|
|
200
|
+
See [TESTING.md](./TESTING.md) for comprehensive testing guide.
|
|
201
|
+
|
|
202
|
+
**Quick Test:**
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Run test suite
|
|
206
|
+
npm test
|
|
207
|
+
|
|
208
|
+
# Start mock server for testing
|
|
209
|
+
npm run test:server
|
|
210
|
+
|
|
211
|
+
# Test directly with verbose output
|
|
212
|
+
PACKAGE_TRACKER_VERBOSE=true \
|
|
213
|
+
PACKAGE_TRACKER_ENDPOINT=https://httpbin.org/post \
|
|
214
|
+
node index.js
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Testing Locally
|
|
218
|
+
|
|
219
|
+
1. Set verbose mode:
|
|
220
|
+
```bash
|
|
221
|
+
export PACKAGE_TRACKER_VERBOSE=true
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
2. Test with a mock endpoint or your backend:
|
|
225
|
+
```bash
|
|
226
|
+
export PACKAGE_TRACKER_ENDPOINT=https://telemetry.reo.dev/data
|
|
227
|
+
npm install
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Debugging
|
|
231
|
+
|
|
232
|
+
Enable verbose logging to see what data is being collected and sent:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
PACKAGE_TRACKER_VERBOSE=true npm install
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
241
|
+
|
|
242
|
+
## Similar Projects
|
|
243
|
+
|
|
244
|
+
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/reo-census",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Census - tracks npm package installations for Reo analytics",
|
|
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": "git+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,299 @@
|
|
|
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
|
+
// Add dependencies if full data mode is enabled
|
|
51
|
+
if (this.config.isFullDataMode()) {
|
|
52
|
+
const dependencies = this.collectDependencies();
|
|
53
|
+
if (dependencies && Object.keys(dependencies).length > 0) {
|
|
54
|
+
data.package.dependencies = dependencies;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return data;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Return minimal data if collection fails
|
|
61
|
+
return {
|
|
62
|
+
package: {
|
|
63
|
+
name: this.config.getPackageName(),
|
|
64
|
+
version: this.config.getPackageVersion(),
|
|
65
|
+
},
|
|
66
|
+
error: 'Collection failed',
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Collect optional metadata (company info, git info, etc.)
|
|
74
|
+
*/
|
|
75
|
+
collectMetadata() {
|
|
76
|
+
const metadata = {};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Try to get git user info
|
|
80
|
+
const gitConfig = this.getGitConfig();
|
|
81
|
+
if (gitConfig.user) {
|
|
82
|
+
metadata.gitUser = gitConfig.user;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Email handling based on data mode
|
|
86
|
+
if (gitConfig.email) {
|
|
87
|
+
if (this.config.isFullDataMode()) {
|
|
88
|
+
// Full mode: send full email
|
|
89
|
+
metadata.email = gitConfig.email;
|
|
90
|
+
} else {
|
|
91
|
+
// Partial mode: only send domain
|
|
92
|
+
const emailDomain = gitConfig.email.split('@')[1];
|
|
93
|
+
if (emailDomain) {
|
|
94
|
+
metadata.emailDomain = emailDomain;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try to detect CI/CD environment
|
|
100
|
+
const ciInfo = this.detectCI();
|
|
101
|
+
if (ciInfo) {
|
|
102
|
+
metadata.ci = ciInfo;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Try to get npm user info (if available)
|
|
106
|
+
const npmUser = process.env.npm_config_user;
|
|
107
|
+
if (npmUser) {
|
|
108
|
+
metadata.npmUser = npmUser;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Debug logging if verbose mode
|
|
112
|
+
if (this.config && this.config.isVerbose()) {
|
|
113
|
+
console.log('[@saurabhreo/reo-census] Git config found:', {
|
|
114
|
+
user: gitConfig.user || 'not found',
|
|
115
|
+
email: gitConfig.email ? 'found (domain extracted)' : 'not found',
|
|
116
|
+
emailDomain: gitConfig.email ? gitConfig.email.split('@')[1] : 'none',
|
|
117
|
+
gitConfigPath: path.join(require('os').homedir(), '.gitconfig')
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Silently fail - metadata is optional
|
|
122
|
+
if (this.config && this.config.isVerbose()) {
|
|
123
|
+
console.log('[@saurabhreo/reo-census] Metadata collection error:', error.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return metadata;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read git config to get user information
|
|
132
|
+
*/
|
|
133
|
+
getGitConfig() {
|
|
134
|
+
const gitConfig = {};
|
|
135
|
+
const gitConfigPath = path.join(os.homedir(), '.gitconfig');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
139
|
+
const content = fs.readFileSync(gitConfigPath, 'utf8');
|
|
140
|
+
|
|
141
|
+
// Parse git config by lines - more robust than regex
|
|
142
|
+
const lines = content.split('\n');
|
|
143
|
+
let inUserSection = false;
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < lines.length; i++) {
|
|
146
|
+
const line = lines[i];
|
|
147
|
+
const trimmedLine = line.trim();
|
|
148
|
+
|
|
149
|
+
// Check if we're entering [user] section
|
|
150
|
+
if (trimmedLine.match(/^\[user\]$/i)) {
|
|
151
|
+
inUserSection = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if we're leaving the section (new section starts)
|
|
156
|
+
if (trimmedLine.match(/^\[/)) {
|
|
157
|
+
inUserSection = false;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If we're in [user] section, parse name and email
|
|
162
|
+
if (inUserSection) {
|
|
163
|
+
// Parse name = value (handle leading whitespace, quotes, spaces, comments)
|
|
164
|
+
// Matches: " name = value", "\tname=value", "name = value # comment"
|
|
165
|
+
const nameMatch = line.match(/^\s*name\s*=\s*(.+?)(?:\s*[#;]|$)/i);
|
|
166
|
+
if (nameMatch && !gitConfig.user) {
|
|
167
|
+
let name = nameMatch[1].trim();
|
|
168
|
+
name = name.replace(/^["']|["']$/g, ''); // Remove quotes
|
|
169
|
+
gitConfig.user = name;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Parse email = value (handle leading whitespace, quotes, spaces, comments)
|
|
173
|
+
const emailMatch = line.match(/^\s*email\s*=\s*(.+?)(?:\s*[#;]|$)/i);
|
|
174
|
+
if (emailMatch && !gitConfig.email) {
|
|
175
|
+
let email = emailMatch[1].trim();
|
|
176
|
+
email = email.replace(/^["']|["']$/g, ''); // Remove quotes
|
|
177
|
+
gitConfig.email = email;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback: try regex if line-by-line parsing didn't work
|
|
183
|
+
if (!gitConfig.user || !gitConfig.email) {
|
|
184
|
+
const userSectionMatch = content.match(/\[user\]([\s\S]*?)(?=\[|$)/i);
|
|
185
|
+
if (userSectionMatch) {
|
|
186
|
+
const userSection = userSectionMatch[1];
|
|
187
|
+
|
|
188
|
+
if (!gitConfig.user) {
|
|
189
|
+
const nameMatch = userSection.match(/name\s*=\s*["']?([^"'\n]+)["']?/i);
|
|
190
|
+
if (nameMatch) {
|
|
191
|
+
gitConfig.user = nameMatch[1].trim();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!gitConfig.email) {
|
|
196
|
+
const emailMatch = userSection.match(/email\s*=\s*["']?([^"'\n]+)["']?/i);
|
|
197
|
+
if (emailMatch) {
|
|
198
|
+
gitConfig.email = emailMatch[1].trim();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Silently fail - git config might not exist or be unreadable
|
|
206
|
+
if (this.config && this.config.isVerbose()) {
|
|
207
|
+
console.log('[@saurabhreo/reo-census] Git config read error:', error.message);
|
|
208
|
+
console.log('[@saurabhreo/reo-census] Git config path:', gitConfigPath);
|
|
209
|
+
console.log('[@saurabhreo/reo-census] Git config exists:', fs.existsSync(gitConfigPath));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Always log in verbose mode for debugging
|
|
214
|
+
if (this.config && this.config.isVerbose()) {
|
|
215
|
+
console.log('[@saurabhreo/reo-census] Git config result:', gitConfig);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return gitConfig;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Detect CI/CD environment
|
|
223
|
+
*/
|
|
224
|
+
detectCI() {
|
|
225
|
+
const ciEnvs = {
|
|
226
|
+
CI: process.env.CI,
|
|
227
|
+
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS ? 'GitHub Actions' : null,
|
|
228
|
+
GITLAB_CI: process.env.GITLAB_CI ? 'GitLab CI' : null,
|
|
229
|
+
JENKINS: process.env.JENKINS_URL ? 'Jenkins' : null,
|
|
230
|
+
TRAVIS: process.env.TRAVIS ? 'Travis CI' : null,
|
|
231
|
+
CIRCLE_CI: process.env.CIRCLE_CI ? 'CircleCI' : null,
|
|
232
|
+
BITBUCKET_BUILD_NUMBER: process.env.BITBUCKET_BUILD_NUMBER ? 'Bitbucket Pipelines' : null,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const detected = Object.entries(ciEnvs)
|
|
236
|
+
.filter(([_, value]) => value !== null && value !== undefined)
|
|
237
|
+
.map(([key, value]) => ({ [key]: value }));
|
|
238
|
+
|
|
239
|
+
return detected.length > 0 ? detected : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Collect dependencies from package.json
|
|
244
|
+
* Only called in full data mode
|
|
245
|
+
*/
|
|
246
|
+
collectDependencies() {
|
|
247
|
+
try {
|
|
248
|
+
const dependencies = {};
|
|
249
|
+
// Get package.json from config
|
|
250
|
+
const packageJson = this.config.packageJson;
|
|
251
|
+
|
|
252
|
+
if (!packageJson) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Collect dependencies
|
|
257
|
+
if (packageJson.dependencies && typeof packageJson.dependencies === 'object') {
|
|
258
|
+
dependencies.dependencies = packageJson.dependencies;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Collect devDependencies
|
|
262
|
+
if (packageJson.devDependencies && typeof packageJson.devDependencies === 'object') {
|
|
263
|
+
dependencies.devDependencies = packageJson.devDependencies;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Collect peerDependencies
|
|
267
|
+
if (packageJson.peerDependencies && typeof packageJson.peerDependencies === 'object') {
|
|
268
|
+
dependencies.peerDependencies = packageJson.peerDependencies;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Collect optionalDependencies
|
|
272
|
+
if (packageJson.optionalDependencies && typeof packageJson.optionalDependencies === 'object') {
|
|
273
|
+
dependencies.optionalDependencies = packageJson.optionalDependencies;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Return null if no dependencies found
|
|
277
|
+
if (Object.keys(dependencies).length === 0) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return dependencies;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// Silently fail - dependencies are optional
|
|
284
|
+
if (this.config && this.config.isVerbose()) {
|
|
285
|
+
console.log('[@saurabhreo/reo-census] Dependency collection error:', error.message);
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Format data for sending (can be overridden for custom formatting)
|
|
293
|
+
*/
|
|
294
|
+
format(data) {
|
|
295
|
+
return JSON.stringify(data);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = Collector;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
* Skips the tracker's own package.json to find the parent package
|
|
18
|
+
*/
|
|
19
|
+
findPackageJson() {
|
|
20
|
+
let currentDir = process.cwd();
|
|
21
|
+
const root = path.parse(currentDir).root;
|
|
22
|
+
const trackerPackageName = '@saurabhreo/reo-census';
|
|
23
|
+
|
|
24
|
+
// If we're in node_modules, walk up until we exit node_modules
|
|
25
|
+
// to find the parent package that installed the tracker
|
|
26
|
+
if (currentDir.includes('node_modules')) {
|
|
27
|
+
// Walk up until we exit node_modules
|
|
28
|
+
// e.g., /project/node_modules/@saurabhreo/reo-census -> /project
|
|
29
|
+
while (currentDir !== root && currentDir.includes('node_modules')) {
|
|
30
|
+
currentDir = path.dirname(currentDir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Now look for package.json in the parent package directory (first one we find)
|
|
34
|
+
// This should be the package that installed the tracker
|
|
35
|
+
while (currentDir !== root) {
|
|
36
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
37
|
+
if (fs.existsSync(packagePath)) {
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(packagePath, 'utf8');
|
|
40
|
+
const pkg = JSON.parse(content);
|
|
41
|
+
// Skip if this is the tracker's own package.json
|
|
42
|
+
if (pkg.name !== trackerPackageName) {
|
|
43
|
+
return packagePath;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Continue searching if package.json is invalid
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
currentDir = path.dirname(currentDir);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Not in node_modules, search normally but skip tracker's package.json
|
|
53
|
+
while (currentDir !== root) {
|
|
54
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
55
|
+
if (fs.existsSync(packagePath)) {
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(packagePath, 'utf8');
|
|
58
|
+
const pkg = JSON.parse(content);
|
|
59
|
+
// Skip if this is the tracker's own package.json
|
|
60
|
+
if (pkg.name !== trackerPackageName) {
|
|
61
|
+
return packagePath;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Continue searching if package.json is invalid
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
currentDir = path.dirname(currentDir);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fallback: return tracker's package.json if parent not found
|
|
72
|
+
return path.join(process.cwd(), 'package.json');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load package.json content
|
|
77
|
+
*/
|
|
78
|
+
loadPackageJson() {
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(this.packageJsonPath)) {
|
|
81
|
+
const content = fs.readFileSync(this.packageJsonPath, 'utf8');
|
|
82
|
+
return JSON.parse(content);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Silently fail - package.json might not exist or be invalid
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if tracking is enabled
|
|
92
|
+
* Respects environment variable and package.json settings
|
|
93
|
+
*/
|
|
94
|
+
isTrackingEnabled() {
|
|
95
|
+
// Check environment variable first (highest priority)
|
|
96
|
+
const envOptOut = process.env.PACKAGE_TRACKER_ANALYTICS === 'false';
|
|
97
|
+
if (envOptOut) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if defaultOptIn is set to false in package.json
|
|
102
|
+
if (this.reoSettings.defaultOptIn === false) {
|
|
103
|
+
// In opt-in mode, check if explicitly enabled
|
|
104
|
+
return process.env.PACKAGE_TRACKER_ANALYTICS === 'true';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default: opt-out mode (enabled by default)
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if verbose logging is enabled
|
|
113
|
+
*/
|
|
114
|
+
isVerbose() {
|
|
115
|
+
return process.env.PACKAGE_TRACKER_VERBOSE === 'true';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get data collection mode
|
|
120
|
+
* Returns 'partial' (default) or 'full'
|
|
121
|
+
* Priority: env var > reoSettings.dataMode in package.json > default ('partial')
|
|
122
|
+
*/
|
|
123
|
+
getDataMode() {
|
|
124
|
+
const mode =
|
|
125
|
+
process.env.PACKAGE_TRACKER_DATA_MODE ||
|
|
126
|
+
this.reoSettings.dataMode ||
|
|
127
|
+
'partial';
|
|
128
|
+
return mode === 'full' ? 'full' : 'partial';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if full data mode is enabled
|
|
133
|
+
*/
|
|
134
|
+
isFullDataMode() {
|
|
135
|
+
return this.getDataMode() === 'full';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get tracking endpoint URL
|
|
140
|
+
* Can be configured via environment variable or package.json
|
|
141
|
+
*/
|
|
142
|
+
getEndpointUrl() {
|
|
143
|
+
return (
|
|
144
|
+
process.env.PACKAGE_TRACKER_ENDPOINT ||
|
|
145
|
+
this.reoSettings.endpoint ||
|
|
146
|
+
'https://telemetry.reo.dev/data' // Default endpoint
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get package name from package.json
|
|
152
|
+
*/
|
|
153
|
+
getPackageName() {
|
|
154
|
+
return this.packageJson?.name || 'unknown';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get package version from package.json
|
|
159
|
+
*/
|
|
160
|
+
getPackageVersion() {
|
|
161
|
+
return this.packageJson?.version || 'unknown';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if this is a top-level installation
|
|
166
|
+
* Reo only tracks when installed as dependency or globally
|
|
167
|
+
*/
|
|
168
|
+
isTopLevelInstall() {
|
|
169
|
+
// Check if we're in node_modules (dependency) or global install
|
|
170
|
+
const cwd = process.cwd();
|
|
171
|
+
const isInNodeModules = cwd.includes('node_modules');
|
|
172
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
173
|
+
|
|
174
|
+
// If not in node_modules and not global, it's likely top-level
|
|
175
|
+
return !isInNodeModules && !isGlobal;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all configuration for logging
|
|
180
|
+
*/
|
|
181
|
+
getConfig() {
|
|
182
|
+
return {
|
|
183
|
+
enabled: this.isTrackingEnabled(),
|
|
184
|
+
verbose: this.isVerbose(),
|
|
185
|
+
endpoint: this.getEndpointUrl(),
|
|
186
|
+
packageName: this.getPackageName(),
|
|
187
|
+
packageVersion: this.getPackageVersion(),
|
|
188
|
+
isTopLevel: this.isTopLevelInstall(),
|
|
189
|
+
dataMode: this.getDataMode(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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': `@saurabhreo/reo-census/${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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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('[@saurabhreo/reo-census] 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;
|