nport 1.0.6 → 2.0.2
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 +227 -32
- package/analytics.js +259 -0
- package/bin-manager.js +378 -0
- package/index.js +635 -0
- package/package.json +65 -33
- package/LICENSE +0 -21
- package/bin/client +0 -43
- package/bin/server +0 -28
- package/client.js +0 -69
- package/examples/api.js +0 -9
- package/lib/api.js +0 -22
- package/nginx.conf.sample +0 -33
- package/server.js +0 -200
package/README.md
CHANGED
|
@@ -1,68 +1,263 @@
|
|
|
1
1
|
# <img src="https://nport.link/assets/imgs/nport-logo.png" height="30" style="vertical-align: middle;"> NPort
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 🚀 Free & open source ngrok alternative - Tunnel localhost to the internet via Cloudflare Edge
|
|
4
4
|
|
|
5
5
|
[](https://github.com/tuanngocptn/nport)
|
|
6
6
|
[](https://www.npmjs.com/package/nport)
|
|
7
7
|
[](https://nport.link)
|
|
8
|
+
[](LICENSE)
|
|
8
9
|
|
|
9
10
|
## What is NPort?
|
|
10
11
|
|
|
11
12
|
[](https://nport.link)
|
|
12
13
|
|
|
13
|
-
NPort
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
14
|
+
NPort is a powerful, lightweight **ngrok alternative** that creates secure HTTP/HTTPS tunnels from your localhost to public URLs using **Cloudflare's global edge network**. No configuration, no accounts, just instant tunnels with custom subdomains!
|
|
15
|
+
|
|
16
|
+
Perfect for:
|
|
17
|
+
- 🚀 **Development environments** - Share your local work instantly
|
|
18
|
+
- 🔒 **Testing webhooks** - Receive webhooks from GitHub, Stripe, PayPal, etc.
|
|
19
|
+
- 📱 **Mobile testing** - Test your web app on real devices
|
|
20
|
+
- 🛠️ **API development** - Debug integrations with external services
|
|
21
|
+
- 👥 **Demo to clients** - Show your progress without deployment
|
|
17
22
|
|
|
18
23
|
## ✨ Features
|
|
19
24
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
+
- ⚡ **Instant Setup**: One command to expose your localhost
|
|
26
|
+
- 🌐 **Custom Subdomains**: Choose your own URL (e.g., `myapp.nport.link`)
|
|
27
|
+
- 🔒 **Automatic HTTPS**: SSL/TLS encryption via Cloudflare
|
|
28
|
+
- 🌍 **Global Edge Network**: Fast connections worldwide via Cloudflare
|
|
29
|
+
- 📡 **WebSocket Support**: Full WebSocket and Server-Sent Events support
|
|
30
|
+
- 🎯 **No Configuration**: Works out of the box
|
|
31
|
+
- 💻 **Cross-Platform**: Windows, macOS, and Linux support
|
|
32
|
+
- 🆓 **100% Free**: No accounts, no limits, no paywalls
|
|
33
|
+
- 🔓 **Open Source**: MIT licensed
|
|
25
34
|
|
|
26
35
|
## 📦 Installation
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# Local installation
|
|
31
|
-
npm install nport
|
|
37
|
+
### NPM (Recommended)
|
|
32
38
|
|
|
39
|
+
```bash
|
|
33
40
|
# Global installation
|
|
34
41
|
npm install -g nport
|
|
42
|
+
|
|
43
|
+
# Or use npx without installation
|
|
44
|
+
npx nport 3000 -s myapp
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# Local installation
|
|
40
|
-
npm install git+https://github.com/tuanngocptn/nport.git
|
|
47
|
+
### From GitHub
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
```bash
|
|
43
50
|
npm install -g git+https://github.com/tuanngocptn/nport.git
|
|
44
51
|
```
|
|
45
52
|
|
|
46
53
|
## 🚀 Quick Start
|
|
47
54
|
|
|
48
55
|
### Basic Usage
|
|
56
|
+
|
|
57
|
+
Expose port 3000 with a random subdomain:
|
|
49
58
|
```bash
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
nport 3000
|
|
60
|
+
```
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
nport -s myapp -p 3000
|
|
62
|
+
Output:
|
|
55
63
|
```
|
|
56
|
-
|
|
64
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
65
|
+
NPort - Free & Open Source ngrok Alternative
|
|
66
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
67
|
+
🌐 Website: https://nport.link
|
|
68
|
+
📦 NPM: https://www.npmjs.com/package/nport
|
|
69
|
+
💻 GitHub: https://github.com/tuanngocptn/nport
|
|
70
|
+
☕ Support: https://buymeacoffee.com/tuanngocptn
|
|
71
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
🚀 Starting Tunnel for port 3000...
|
|
74
|
+
✔ Tunnel created!
|
|
75
|
+
🌍 Public URL: https://user-1234.nport.link
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Custom Subdomain
|
|
79
|
+
|
|
80
|
+
Choose your own subdomain:
|
|
81
|
+
```bash
|
|
82
|
+
nport 3000 -s myapp
|
|
83
|
+
# Creates: https://myapp.nport.link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or using long form:
|
|
59
87
|
```bash
|
|
60
|
-
|
|
61
|
-
npx nport --server https://server.nport.link \
|
|
62
|
-
--subdomain myapp \
|
|
63
|
-
--hostname 127.0.0.1 \
|
|
64
|
-
--port 3000
|
|
88
|
+
nport 3000 --subdomain myapp
|
|
65
89
|
```
|
|
66
90
|
|
|
67
|
-
##
|
|
68
|
-
|
|
91
|
+
## 📖 Usage Examples
|
|
92
|
+
|
|
93
|
+
### Web Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Next.js
|
|
97
|
+
npm run dev
|
|
98
|
+
nport 3000 -s my-nextjs-app
|
|
99
|
+
|
|
100
|
+
# React (Create React App)
|
|
101
|
+
npm start
|
|
102
|
+
nport 3000 -s my-react-app
|
|
103
|
+
|
|
104
|
+
# Vue.js
|
|
105
|
+
npm run dev
|
|
106
|
+
nport 8080 -s my-vue-app
|
|
107
|
+
|
|
108
|
+
# Express.js
|
|
109
|
+
node server.js
|
|
110
|
+
nport 3000 -s my-api
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Webhook Testing
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Start your local webhook receiver
|
|
117
|
+
node webhook-receiver.js
|
|
118
|
+
|
|
119
|
+
# Expose it to the internet
|
|
120
|
+
nport 4000 -s my-webhooks
|
|
121
|
+
|
|
122
|
+
# Use in GitHub webhook settings:
|
|
123
|
+
# https://my-webhooks.nport.link/webhook
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Mobile Device Testing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Start your local dev server
|
|
130
|
+
npm run dev
|
|
131
|
+
|
|
132
|
+
# Create tunnel
|
|
133
|
+
nport 3000 -s mobile-test
|
|
134
|
+
|
|
135
|
+
# Open on your phone:
|
|
136
|
+
# https://mobile-test.nport.link
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 🎯 CLI Options
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
nport <port> [options]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
| Option | Short | Description | Example |
|
|
146
|
+
|--------|-------|-------------|---------|
|
|
147
|
+
| `<port>` | - | Local port to tunnel (default: 8080) | `nport 3000` |
|
|
148
|
+
| `--subdomain` | `-s` | Custom subdomain | `nport 3000 -s myapp` |
|
|
149
|
+
| `-s=value` | - | Alternative format | `nport 3000 -s=myapp` |
|
|
150
|
+
|
|
151
|
+
## 🔧 How It Works
|
|
152
|
+
|
|
153
|
+
1. **You run** `nport 3000 -s myapp`
|
|
154
|
+
2. **NPort creates** a Cloudflare Tunnel
|
|
155
|
+
3. **DNS record** is created: `myapp.nport.link` → Cloudflare Edge
|
|
156
|
+
4. **Cloudflared binary** connects your localhost:3000 to Cloudflare
|
|
157
|
+
5. **Traffic flows** through Cloudflare's global network to your machine
|
|
158
|
+
6. **On exit** (Ctrl+C), tunnel and DNS are automatically cleaned up
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Internet → Cloudflare Edge → Cloudflare Tunnel → Your localhost:3000
|
|
162
|
+
(https://myapp.nport.link)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 🛡️ Security
|
|
166
|
+
|
|
167
|
+
- **HTTPS by default**: All tunnels use SSL/TLS encryption
|
|
168
|
+
- **Cloudflare protection**: DDoS protection and security features
|
|
169
|
+
- **Automatic cleanup**: Tunnels are removed when you stop the process
|
|
170
|
+
- **No data logging**: We don't store or log your traffic
|
|
171
|
+
|
|
172
|
+
## 🆚 Comparison with ngrok
|
|
173
|
+
|
|
174
|
+
| Feature | NPort | ngrok |
|
|
175
|
+
|---------|-------|-------|
|
|
176
|
+
| Price | 100% Free | Free tier limited |
|
|
177
|
+
| Custom subdomains | ✅ Always | ❌ Paid only |
|
|
178
|
+
| HTTPS | ✅ Always | ✅ |
|
|
179
|
+
| Account required | ❌ No | ✅ Yes |
|
|
180
|
+
| Time limits | ❌ None | ⚠️ Free tier limited |
|
|
181
|
+
| Open source | ✅ MIT | ❌ Proprietary |
|
|
182
|
+
| Global network | ✅ Cloudflare | ✅ ngrok Edge |
|
|
183
|
+
|
|
184
|
+
## 🧹 Cleanup
|
|
185
|
+
|
|
186
|
+
NPort automatically cleans up resources when you:
|
|
187
|
+
- Press **Ctrl+C** to exit
|
|
188
|
+
- Kill the process
|
|
189
|
+
- Terminal closes
|
|
190
|
+
|
|
191
|
+
The cleanup process:
|
|
192
|
+
1. ✅ Deletes DNS record (`myapp.nport.link`)
|
|
193
|
+
2. ✅ Removes Cloudflare Tunnel
|
|
194
|
+
3. ✅ Stops cloudflared process
|
|
195
|
+
|
|
196
|
+
## 🐛 Troubleshooting
|
|
197
|
+
|
|
198
|
+
### Binary not found
|
|
199
|
+
|
|
200
|
+
If you see "Cloudflared binary not found":
|
|
201
|
+
```bash
|
|
202
|
+
npm install -g nport --force
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Port already in use
|
|
206
|
+
|
|
207
|
+
Make sure your local server is running on the specified port:
|
|
208
|
+
```bash
|
|
209
|
+
# Check if something is listening on port 3000
|
|
210
|
+
lsof -i :3000 # macOS/Linux
|
|
211
|
+
netstat -ano | findstr :3000 # Windows
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Subdomain already taken
|
|
215
|
+
|
|
216
|
+
Choose a different subdomain name:
|
|
217
|
+
```bash
|
|
218
|
+
nport 3000 -s myapp-v2
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Connection issues
|
|
222
|
+
|
|
223
|
+
The `ERR Cannot determine default origin certificate path` warning is harmless and can be ignored. It appears because cloudflared checks for certificate-based authentication (we use token-based instead).
|
|
224
|
+
|
|
225
|
+
## 🤝 Contributing
|
|
226
|
+
|
|
227
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
228
|
+
|
|
229
|
+
1. Fork the repository
|
|
230
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
231
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
232
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
233
|
+
5. Open a Pull Request
|
|
234
|
+
|
|
235
|
+
## 💖 Support
|
|
236
|
+
|
|
237
|
+
If you find NPort useful, please consider supporting the project:
|
|
238
|
+
|
|
239
|
+
- ⭐ [Star on GitHub](https://github.com/tuanngocptn/nport)
|
|
240
|
+
- ☕ [Buy me a coffee](https://buymeacoffee.com/tuanngocptn)
|
|
241
|
+
- 💬 Share with your friends and colleagues
|
|
242
|
+
- 🐛 [Report bugs](https://github.com/tuanngocptn/nport/issues)
|
|
243
|
+
|
|
244
|
+
## 📄 License
|
|
245
|
+
|
|
246
|
+
[MIT License](LICENSE) - Feel free to use NPort in your projects!
|
|
247
|
+
|
|
248
|
+
## 🙏 Credits
|
|
249
|
+
|
|
250
|
+
- Created by [Nick Pham](https://github.com/tuanngocptn) 🇻🇳
|
|
251
|
+
- Powered by [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
|
|
252
|
+
- Inspired by [ngrok](https://ngrok.com) and [localtunnel](https://github.com/localtunnel/localtunnel)
|
|
253
|
+
|
|
254
|
+
## 🔗 Links
|
|
255
|
+
|
|
256
|
+
- 🌐 Website: [https://nport.link](https://nport.link)
|
|
257
|
+
- 📦 NPM: [https://www.npmjs.com/package/nport](https://www.npmjs.com/package/nport)
|
|
258
|
+
- 💻 GitHub: [https://github.com/tuanngocptn/nport](https://github.com/tuanngocptn/nport)
|
|
259
|
+
- 📧 Email: tuanngocptn@gmail.com
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
Made with ❤️ by [Nick Pham](https://github.com/tuanngocptn)
|
package/analytics.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Firebase Analytics for CLI
|
|
3
|
+
// Using Google Analytics 4 Measurement Protocol
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import { createHash, randomUUID } from "crypto";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Firebase/GA4 Configuration (from website/home.html)
|
|
17
|
+
// Full Firebase config for reference (if needed for future features)
|
|
18
|
+
const FIREBASE_WEB_CONFIG = {
|
|
19
|
+
apiKey: "AIzaSyArRxHZJUt4o2RxiLqX1yDSkuUd6ZFy45I",
|
|
20
|
+
authDomain: "nport-link.firebaseapp.com",
|
|
21
|
+
projectId: "nport-link",
|
|
22
|
+
storageBucket: "nport-link.firebasestorage.app",
|
|
23
|
+
messagingSenderId: "515584605320",
|
|
24
|
+
appId: "1:515584605320:web:88daabc8d77146c6e7f33d",
|
|
25
|
+
measurementId: "G-8MYXZL6PGD"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Analytics-specific config (for GA4 Measurement Protocol)
|
|
29
|
+
const FIREBASE_CONFIG = {
|
|
30
|
+
measurementId: FIREBASE_WEB_CONFIG.measurementId,
|
|
31
|
+
apiSecret: process.env.NPORT_ANALYTICS_SECRET || "YOUR_API_SECRET_HERE", // Get from Firebase Console
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Analytics Configuration
|
|
35
|
+
const ANALYTICS_CONFIG = {
|
|
36
|
+
enabled: true, // Can be disabled by environment variable
|
|
37
|
+
debug: process.env.NPORT_DEBUG === "true",
|
|
38
|
+
timeout: 2000, // Don't block CLI for too long
|
|
39
|
+
userIdFile: path.join(os.homedir(), ".nport-analytics"),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Analytics Manager
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
class AnalyticsManager {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.userId = null;
|
|
49
|
+
this.sessionId = null;
|
|
50
|
+
this.disabled = false;
|
|
51
|
+
|
|
52
|
+
// Disable analytics if environment variable is set
|
|
53
|
+
if (process.env.NPORT_ANALYTICS === "false") {
|
|
54
|
+
this.disabled = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize analytics - must be called before tracking
|
|
60
|
+
*/
|
|
61
|
+
async initialize() {
|
|
62
|
+
if (this.disabled) return;
|
|
63
|
+
|
|
64
|
+
// Check if API secret is configured
|
|
65
|
+
if (!FIREBASE_CONFIG.apiSecret || FIREBASE_CONFIG.apiSecret === "YOUR_API_SECRET_HERE") {
|
|
66
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
67
|
+
console.warn("[Analytics] API secret not configured. Analytics disabled.");
|
|
68
|
+
console.warn("[Analytics] Set NPORT_ANALYTICS_SECRET environment variable.");
|
|
69
|
+
}
|
|
70
|
+
this.disabled = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
this.userId = await this.getUserId();
|
|
76
|
+
this.sessionId = this.generateSessionId();
|
|
77
|
+
|
|
78
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
79
|
+
console.log("[Analytics] Initialized successfully");
|
|
80
|
+
console.log("[Analytics] User ID:", this.userId.substring(0, 8) + "...");
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
84
|
+
console.error("[Analytics] Initialization failed:", error.message);
|
|
85
|
+
}
|
|
86
|
+
this.disabled = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get or create a persistent user ID
|
|
92
|
+
*/
|
|
93
|
+
async getUserId() {
|
|
94
|
+
try {
|
|
95
|
+
// Try to read existing user ID
|
|
96
|
+
if (fs.existsSync(ANALYTICS_CONFIG.userIdFile)) {
|
|
97
|
+
const userId = fs.readFileSync(ANALYTICS_CONFIG.userIdFile, "utf8").trim();
|
|
98
|
+
if (userId) return userId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Generate new anonymous user ID
|
|
102
|
+
const userId = this.generateAnonymousId();
|
|
103
|
+
|
|
104
|
+
// Save for future use
|
|
105
|
+
fs.writeFileSync(ANALYTICS_CONFIG.userIdFile, userId, "utf8");
|
|
106
|
+
|
|
107
|
+
return userId;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// If file operations fail, use session-based ID
|
|
110
|
+
return this.generateAnonymousId();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate anonymous user ID based on machine characteristics
|
|
116
|
+
*/
|
|
117
|
+
generateAnonymousId() {
|
|
118
|
+
const machineId = [
|
|
119
|
+
os.hostname(),
|
|
120
|
+
os.platform(),
|
|
121
|
+
os.arch(),
|
|
122
|
+
os.homedir(),
|
|
123
|
+
].join("-");
|
|
124
|
+
|
|
125
|
+
return createHash("sha256").update(machineId).digest("hex").substring(0, 32);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate session ID
|
|
130
|
+
*/
|
|
131
|
+
generateSessionId() {
|
|
132
|
+
return randomUUID();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Track an event
|
|
137
|
+
*/
|
|
138
|
+
async trackEvent(eventName, params = {}) {
|
|
139
|
+
if (this.disabled || !ANALYTICS_CONFIG.enabled) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const payload = this.buildPayload(eventName, params);
|
|
143
|
+
|
|
144
|
+
// Send to GA4 Measurement Protocol (non-blocking)
|
|
145
|
+
axios.post(
|
|
146
|
+
`https://www.google-analytics.com/mp/collect?measurement_id=${FIREBASE_CONFIG.measurementId}&api_secret=${FIREBASE_CONFIG.apiSecret}`,
|
|
147
|
+
payload,
|
|
148
|
+
{
|
|
149
|
+
timeout: ANALYTICS_CONFIG.timeout,
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
}
|
|
152
|
+
).catch((error) => {
|
|
153
|
+
// Silently fail - don't interrupt CLI operations
|
|
154
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
155
|
+
console.error("[Analytics] Failed to send event:", error.message);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
160
|
+
console.log("[Analytics] Event tracked:", eventName, params);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Silently fail
|
|
164
|
+
if (ANALYTICS_CONFIG.debug) {
|
|
165
|
+
console.error("[Analytics] Error tracking event:", error.message);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build GA4 Measurement Protocol payload
|
|
172
|
+
*/
|
|
173
|
+
buildPayload(eventName, params) {
|
|
174
|
+
return {
|
|
175
|
+
client_id: this.userId,
|
|
176
|
+
events: [
|
|
177
|
+
{
|
|
178
|
+
name: eventName,
|
|
179
|
+
params: {
|
|
180
|
+
session_id: this.sessionId,
|
|
181
|
+
engagement_time_msec: "100",
|
|
182
|
+
...this.getSystemInfo(),
|
|
183
|
+
...params,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get system information for context
|
|
192
|
+
*/
|
|
193
|
+
getSystemInfo() {
|
|
194
|
+
return {
|
|
195
|
+
os_platform: os.platform(),
|
|
196
|
+
os_version: os.release(),
|
|
197
|
+
os_arch: os.arch(),
|
|
198
|
+
node_version: process.version,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Track CLI start
|
|
204
|
+
*/
|
|
205
|
+
async trackCliStart(port, subdomain, version) {
|
|
206
|
+
await this.trackEvent("cli_start", {
|
|
207
|
+
port: String(port),
|
|
208
|
+
has_custom_subdomain: subdomain && !subdomain.startsWith("user-"),
|
|
209
|
+
cli_version: version,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Track tunnel creation
|
|
215
|
+
*/
|
|
216
|
+
async trackTunnelCreated(subdomain, port) {
|
|
217
|
+
await this.trackEvent("tunnel_created", {
|
|
218
|
+
subdomain_type: subdomain.startsWith("user-") ? "random" : "custom",
|
|
219
|
+
port: String(port),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Track tunnel error
|
|
225
|
+
*/
|
|
226
|
+
async trackTunnelError(errorType, errorMessage) {
|
|
227
|
+
await this.trackEvent("tunnel_error", {
|
|
228
|
+
error_type: errorType,
|
|
229
|
+
error_message: errorMessage.substring(0, 100), // Limit length
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Track tunnel shutdown
|
|
235
|
+
*/
|
|
236
|
+
async trackTunnelShutdown(reason, durationSeconds) {
|
|
237
|
+
await this.trackEvent("tunnel_shutdown", {
|
|
238
|
+
shutdown_reason: reason, // "manual", "timeout", "error"
|
|
239
|
+
duration_seconds: String(Math.floor(durationSeconds)),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Track CLI update notification shown
|
|
245
|
+
*/
|
|
246
|
+
async trackUpdateAvailable(currentVersion, latestVersion) {
|
|
247
|
+
await this.trackEvent("update_available", {
|
|
248
|
+
current_version: currentVersion,
|
|
249
|
+
latest_version: latestVersion,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Export singleton instance
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
export const analytics = new AnalyticsManager();
|
|
259
|
+
|