healcode-client 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 +110 -0
- package/bin/healcode.js +200 -0
- package/bin/postinstall.js +235 -0
- package/dist/index.d.mts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +48 -0
- package/urls.config.json +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @healcode/client
|
|
2
|
+
|
|
3
|
+
🩺 **HealCode** - Automatic error detection and fix generation for your frontend projects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @healcode/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Initialize in your project
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx healcode init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This will prompt you for your HealCode token and create a `healcode.config.json` file.
|
|
20
|
+
|
|
21
|
+
### 2. Import in your app
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// main.ts or index.js
|
|
25
|
+
import { initFromConfig } from '@healcode/client';
|
|
26
|
+
|
|
27
|
+
initFromConfig();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with manual configuration:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { HealCode } from '@healcode/client';
|
|
34
|
+
|
|
35
|
+
const healcode = new HealCode({
|
|
36
|
+
token: 'hc_your_token_here',
|
|
37
|
+
endpoint: 'https://api.healcode.io', // optional
|
|
38
|
+
enabled: true,
|
|
39
|
+
captureConsoleErrors: true,
|
|
40
|
+
captureUnhandledRejections: true,
|
|
41
|
+
maxBreadcrumbs: 20,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- 🔍 **Automatic Error Capture** - Catches window errors, console.error, and unhandled rejections
|
|
48
|
+
- 🍞 **Breadcrumbs** - Tracks user interactions leading up to errors
|
|
49
|
+
- 🔧 **Auto-Fix** - Generates and creates PRs with fixes automatically
|
|
50
|
+
- 🔒 **Secure** - Your repository tokens are encrypted
|
|
51
|
+
- 📊 **Dashboard** - View all errors and fixes at healcode.io
|
|
52
|
+
|
|
53
|
+
## CLI Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx healcode init # Initialize HealCode in your project
|
|
57
|
+
npx healcode status # Check configuration status
|
|
58
|
+
npx healcode enable # Enable error tracking
|
|
59
|
+
npx healcode disable # Disable error tracking
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Manual Error Capture
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { captureError, addBreadcrumb } from '@healcode/client';
|
|
66
|
+
|
|
67
|
+
// Add custom breadcrumb
|
|
68
|
+
addBreadcrumb('user.action', 'User clicked checkout button', { cartItems: 3 });
|
|
69
|
+
|
|
70
|
+
// Manually capture an error
|
|
71
|
+
try {
|
|
72
|
+
riskyOperation();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
captureError(error.message, error.stack);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration Options
|
|
79
|
+
|
|
80
|
+
| Option | Type | Default | Description |
|
|
81
|
+
|--------|------|---------|-------------|
|
|
82
|
+
| `token` | string | required | Your HealCode integration token |
|
|
83
|
+
| `endpoint` | string | `https://api.healcode.io` | API endpoint |
|
|
84
|
+
| `enabled` | boolean | `true` | Enable/disable tracking |
|
|
85
|
+
| `captureConsoleErrors` | boolean | `true` | Capture console.error calls |
|
|
86
|
+
| `captureUnhandledRejections` | boolean | `true` | Capture unhandled promise rejections |
|
|
87
|
+
| `maxBreadcrumbs` | number | `20` | Maximum breadcrumbs to store |
|
|
88
|
+
| `beforeSend` | function | - | Modify or filter errors before sending |
|
|
89
|
+
|
|
90
|
+
## beforeSend Hook
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const healcode = new HealCode({
|
|
94
|
+
token: 'hc_...',
|
|
95
|
+
beforeSend: (payload) => {
|
|
96
|
+
// Filter out specific errors
|
|
97
|
+
if (payload.message.includes('ResizeObserver')) {
|
|
98
|
+
return null; // Don't send
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Modify payload
|
|
102
|
+
payload.message = payload.message.replace(/secret/gi, '[REDACTED]');
|
|
103
|
+
return payload;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/bin/healcode.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILE_NAME = 'healcode.config.json';
|
|
10
|
+
const DEFAULT_ENDPOINT = 'https://api.healcode.io';
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('healcode')
|
|
16
|
+
.description('HealCode CLI - Automatic error detection and fix generation')
|
|
17
|
+
.version('1.0.0');
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('init')
|
|
21
|
+
.description('Initialize HealCode in your project')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
console.log(chalk.green.bold('\n🩺 HealCode Setup\n'));
|
|
24
|
+
|
|
25
|
+
const initialAnswers = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'input',
|
|
28
|
+
name: 'token',
|
|
29
|
+
message: 'Enter your HealCode token:',
|
|
30
|
+
validate: (input) => {
|
|
31
|
+
if (!input || input.trim().length === 0) {
|
|
32
|
+
return 'Token is required. Get one at https://healcode.io/dashboard';
|
|
33
|
+
}
|
|
34
|
+
if (!input.startsWith('hc_')) {
|
|
35
|
+
return 'Invalid token format. Token should start with "hc_"';
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'input',
|
|
42
|
+
name: 'endpoint',
|
|
43
|
+
message: 'API Endpoint (press Enter for default):',
|
|
44
|
+
default: DEFAULT_ENDPOINT,
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Validate token with API
|
|
49
|
+
console.log(chalk.yellow('\n⏳ Validating token...'));
|
|
50
|
+
|
|
51
|
+
let remoteConfig = null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${initialAnswers.endpoint}/api/tokens/validate`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ token: initialAnswers.token }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await response.json();
|
|
61
|
+
|
|
62
|
+
if (!result.isValid) {
|
|
63
|
+
console.log(chalk.red(`\n❌ Token validation failed: ${result.errorMessage || 'Invalid token'}`));
|
|
64
|
+
console.log(chalk.gray('Get a valid token at https://healcode.io/dashboard\n'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.green('✅ Token validated successfully!'));
|
|
69
|
+
console.log(chalk.gray(` Project: ${result.projectId}`));
|
|
70
|
+
|
|
71
|
+
if (result.configuration) {
|
|
72
|
+
remoteConfig = result.configuration;
|
|
73
|
+
console.log(chalk.green('✅ Configuration loaded from Dashboard.'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.log(chalk.yellow('\n⚠️ Could not validate token (API unreachable). Proceeding with manual setup...'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let options = {};
|
|
81
|
+
|
|
82
|
+
if (remoteConfig) {
|
|
83
|
+
options = remoteConfig;
|
|
84
|
+
} else {
|
|
85
|
+
options = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'confirm',
|
|
88
|
+
name: 'captureConsoleErrors',
|
|
89
|
+
message: 'Capture console.error calls?',
|
|
90
|
+
default: true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'captureUnhandledRejections',
|
|
95
|
+
message: 'Capture unhandled promise rejections?',
|
|
96
|
+
default: true,
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Create config file
|
|
102
|
+
const config = {
|
|
103
|
+
token: initialAnswers.token,
|
|
104
|
+
endpoint: initialAnswers.endpoint === DEFAULT_ENDPOINT ? undefined : initialAnswers.endpoint,
|
|
105
|
+
enabled: true,
|
|
106
|
+
options: {
|
|
107
|
+
captureConsoleErrors: options.captureConsoleErrors !== false,
|
|
108
|
+
captureUnhandledRejections: options.captureUnhandledRejections !== false,
|
|
109
|
+
maxBreadcrumbs: 20,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
114
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
115
|
+
|
|
116
|
+
console.log(chalk.green(`\n✅ Config saved to ${CONFIG_FILE_NAME}`));
|
|
117
|
+
|
|
118
|
+
// Show usage instructions
|
|
119
|
+
console.log(chalk.cyan('\n📖 Usage:\n'));
|
|
120
|
+
console.log(chalk.white(' // In your main entry file (e.g., main.ts, index.js)'));
|
|
121
|
+
console.log(chalk.gray(' import { initFromConfig } from \'@healcode/client\';'));
|
|
122
|
+
console.log(chalk.gray(' initFromConfig();\n'));
|
|
123
|
+
|
|
124
|
+
console.log(chalk.white(' // Or with manual configuration:'));
|
|
125
|
+
console.log(chalk.gray(' import { HealCode } from \'@healcode/client\';'));
|
|
126
|
+
console.log(chalk.gray(` const healcode = new HealCode({ token: '${answers.token.substring(0, 10)}...' });\n`));
|
|
127
|
+
|
|
128
|
+
// Add to .gitignore
|
|
129
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
130
|
+
if (fs.existsSync(gitignorePath)) {
|
|
131
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
132
|
+
if (!gitignore.includes(CONFIG_FILE_NAME)) {
|
|
133
|
+
fs.appendFileSync(gitignorePath, `\n# HealCode\n${CONFIG_FILE_NAME}\n`);
|
|
134
|
+
console.log(chalk.gray(`Added ${CONFIG_FILE_NAME} to .gitignore`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(chalk.green.bold('\n🎉 HealCode is ready! Errors will be automatically tracked and fixed.\n'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command('status')
|
|
143
|
+
.description('Check HealCode configuration status')
|
|
144
|
+
.action(() => {
|
|
145
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(configPath)) {
|
|
148
|
+
console.log(chalk.yellow('\n⚠️ No HealCode configuration found.'));
|
|
149
|
+
console.log(chalk.gray('Run `npx healcode init` to set up.\n'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
155
|
+
console.log(chalk.green.bold('\n🩺 HealCode Status\n'));
|
|
156
|
+
console.log(chalk.white(` Token: ${config.token.substring(0, 15)}...`));
|
|
157
|
+
console.log(chalk.white(` Endpoint: ${config.endpoint || DEFAULT_ENDPOINT}`));
|
|
158
|
+
console.log(chalk.white(` Enabled: ${config.enabled !== false ? '✅' : '❌'}`));
|
|
159
|
+
console.log('');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.log(chalk.red('\n❌ Failed to read configuration file.\n'));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command('disable')
|
|
167
|
+
.description('Disable HealCode error tracking')
|
|
168
|
+
.action(() => {
|
|
169
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(configPath)) {
|
|
172
|
+
console.log(chalk.yellow('\n⚠️ No HealCode configuration found.\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
177
|
+
config.enabled = false;
|
|
178
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
179
|
+
console.log(chalk.yellow('\n⏸️ HealCode tracking disabled.\n'));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
program
|
|
183
|
+
.command('enable')
|
|
184
|
+
.description('Enable HealCode error tracking')
|
|
185
|
+
.action(() => {
|
|
186
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE_NAME);
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(configPath)) {
|
|
189
|
+
console.log(chalk.yellow('\n⚠️ No HealCode configuration found.'));
|
|
190
|
+
console.log(chalk.gray('Run `npx healcode init` to set up.\n'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
195
|
+
config.enabled = true;
|
|
196
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
197
|
+
console.log(chalk.green('\n▶️ HealCode tracking enabled.\n'));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
program.parse();
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HealCode - Postinstall Script
|
|
5
|
+
* This script runs automatically after npm install
|
|
6
|
+
* Prompts for token and configures the library
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
|
|
15
|
+
const CONFIG_FILE_NAME = 'healcode.config.json';
|
|
16
|
+
|
|
17
|
+
// Load URLs from config
|
|
18
|
+
let urlsConfig = { backendUrl: 'https://api.healcode.io', agentUrl: 'https://api.healcode.io' };
|
|
19
|
+
try {
|
|
20
|
+
const urlsConfigPath = path.join(__dirname, '..', 'urls.config.json');
|
|
21
|
+
if (fs.existsSync(urlsConfigPath)) {
|
|
22
|
+
urlsConfig = JSON.parse(fs.readFileSync(urlsConfigPath, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// Use defaults
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_BACKEND_URL = urlsConfig.backendUrl;
|
|
29
|
+
const DEFAULT_AGENT_URL = urlsConfig.agentUrl;
|
|
30
|
+
|
|
31
|
+
// ANSI Colors
|
|
32
|
+
const colors = {
|
|
33
|
+
green: '\x1b[32m',
|
|
34
|
+
yellow: '\x1b[33m',
|
|
35
|
+
red: '\x1b[31m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
gray: '\x1b[90m',
|
|
38
|
+
bold: '\x1b[1m',
|
|
39
|
+
reset: '\x1b[0m'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function log(color, message) {
|
|
43
|
+
console.log(`${color}${message}${colors.reset}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findProjectRoot() {
|
|
47
|
+
// When installed via npm, we need to go up from node_modules/@healcode/client
|
|
48
|
+
let dir = process.cwd();
|
|
49
|
+
|
|
50
|
+
// If we're inside node_modules, go up to project root
|
|
51
|
+
if (dir.includes('node_modules')) {
|
|
52
|
+
const parts = dir.split('node_modules');
|
|
53
|
+
dir = parts[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function configExists() {
|
|
60
|
+
const projectRoot = findProjectRoot();
|
|
61
|
+
const configPath = path.join(projectRoot, CONFIG_FILE_NAME);
|
|
62
|
+
return fs.existsSync(configPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateToken(token, endpoint) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const url = new URL(`${endpoint}/api/Tokens/validate`);
|
|
68
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
69
|
+
|
|
70
|
+
const postData = JSON.stringify({ token: token });
|
|
71
|
+
|
|
72
|
+
const options = {
|
|
73
|
+
hostname: url.hostname,
|
|
74
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
75
|
+
path: url.pathname,
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
80
|
+
},
|
|
81
|
+
timeout: 10000
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const req = protocol.request(options, (res) => {
|
|
85
|
+
let data = '';
|
|
86
|
+
res.on('data', chunk => data += chunk);
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try {
|
|
89
|
+
const result = JSON.parse(data);
|
|
90
|
+
resolve(result);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
reject(new Error('Invalid response from server'));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
req.on('error', reject);
|
|
98
|
+
req.on('timeout', () => {
|
|
99
|
+
req.destroy();
|
|
100
|
+
reject(new Error('Request timed out'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.write(postData);
|
|
104
|
+
req.end();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
// Skip if running in CI or non-interactive mode
|
|
110
|
+
if (process.env.CI || process.env.HEALCODE_SKIP_SETUP || !process.stdin.isTTY) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Skip if we're in development mode (not installed as a dependency)
|
|
115
|
+
// When installed as dependency, __dirname will be inside node_modules
|
|
116
|
+
const currentDir = __dirname;
|
|
117
|
+
if (!currentDir.includes('node_modules')) {
|
|
118
|
+
// We're in the library's own development directory, skip setup
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip if config already exists
|
|
123
|
+
if (configExists()) {
|
|
124
|
+
log(colors.gray, '\n[HealCode] Configuration already exists. Skipping setup.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('');
|
|
129
|
+
log(colors.green + colors.bold, '🩺 HealCode Setup');
|
|
130
|
+
console.log('');
|
|
131
|
+
|
|
132
|
+
const rl = readline.createInterface({
|
|
133
|
+
input: process.stdin,
|
|
134
|
+
output: process.stdout
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const question = (prompt) => new Promise((resolve) => {
|
|
138
|
+
rl.question(prompt, resolve);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Get token
|
|
143
|
+
const token = await question(`${colors.cyan}Enter your HealCode token: ${colors.reset}`);
|
|
144
|
+
|
|
145
|
+
if (!token || token.trim().length === 0) {
|
|
146
|
+
log(colors.yellow, '\n⚠️ No token provided. Run "npx healcode init" later to configure.');
|
|
147
|
+
rl.close();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get endpoint (with default)
|
|
152
|
+
let endpoint = await question(`${colors.cyan}API Endpoint ${colors.gray}(press Enter for default)${colors.cyan}: ${colors.reset}`);
|
|
153
|
+
|
|
154
|
+
if (!endpoint || endpoint.trim().length === 0) {
|
|
155
|
+
// Default endpoint - use configured backend URL
|
|
156
|
+
endpoint = DEFAULT_BACKEND_URL;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
log(colors.yellow, '\n⏳ Validating token...');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = await validateToken(token.trim(), endpoint.trim());
|
|
163
|
+
|
|
164
|
+
if (!result.isValid) {
|
|
165
|
+
log(colors.red, `\n❌ Token validation failed: ${result.errorMessage || 'Invalid token'}`);
|
|
166
|
+
log(colors.gray, 'Get a valid token at your HealCode dashboard\n');
|
|
167
|
+
rl.close();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
log(colors.green, '✅ Token validated successfully!');
|
|
172
|
+
if (result.projectId) {
|
|
173
|
+
log(colors.gray, ` Project ID: ${result.projectId}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get agent endpoint for error reporting (from server response or use default)
|
|
177
|
+
let agentEndpoint = result.agentEndpoint || DEFAULT_AGENT_URL;
|
|
178
|
+
|
|
179
|
+
// Create config
|
|
180
|
+
const projectRoot = findProjectRoot();
|
|
181
|
+
const config = {
|
|
182
|
+
token: token.trim(),
|
|
183
|
+
endpoint: agentEndpoint,
|
|
184
|
+
enabled: true,
|
|
185
|
+
options: {
|
|
186
|
+
captureConsoleErrors: true,
|
|
187
|
+
captureUnhandledRejections: true,
|
|
188
|
+
maxBreadcrumbs: 20
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Apply remote configuration if available
|
|
193
|
+
if (result.configuration) {
|
|
194
|
+
Object.assign(config.options, result.configuration);
|
|
195
|
+
log(colors.green, '✅ Configuration loaded from Dashboard.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const configPath = path.join(projectRoot, CONFIG_FILE_NAME);
|
|
199
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
200
|
+
|
|
201
|
+
log(colors.green, `\n✅ Config saved to ${CONFIG_FILE_NAME}`);
|
|
202
|
+
|
|
203
|
+
// Add to .gitignore
|
|
204
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
205
|
+
if (fs.existsSync(gitignorePath)) {
|
|
206
|
+
let gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
207
|
+
if (!gitignore.includes(CONFIG_FILE_NAME)) {
|
|
208
|
+
fs.appendFileSync(gitignorePath, `\n# HealCode\n${CONFIG_FILE_NAME}\n`);
|
|
209
|
+
log(colors.gray, `Added ${CONFIG_FILE_NAME} to .gitignore`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Show usage
|
|
214
|
+
console.log('');
|
|
215
|
+
log(colors.cyan, '📖 Usage:');
|
|
216
|
+
console.log('');
|
|
217
|
+
log(colors.gray, ' // In your main entry file (e.g., main.ts, index.js)');
|
|
218
|
+
log(colors.gray, " import { initFromConfig } from '@healcode/client';");
|
|
219
|
+
log(colors.gray, ' initFromConfig();');
|
|
220
|
+
console.log('');
|
|
221
|
+
log(colors.green + colors.bold, '🎉 HealCode is ready! Errors will be automatically tracked.\n');
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
log(colors.yellow, `\n⚠️ Could not validate token: ${error.message}`);
|
|
225
|
+
log(colors.gray, 'You can configure manually later with "npx healcode init"\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
} finally {
|
|
229
|
+
rl.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main().catch(() => {
|
|
234
|
+
// Silent fail - postinstall should not break npm install
|
|
235
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
interface HealCodeConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
captureConsoleErrors?: boolean;
|
|
6
|
+
captureUnhandledRejections?: boolean;
|
|
7
|
+
maxBreadcrumbs?: number;
|
|
8
|
+
beforeSend?: (payload: ErrorPayload) => ErrorPayload | null;
|
|
9
|
+
}
|
|
10
|
+
interface HealCodeFileConfig {
|
|
11
|
+
token: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
options?: {
|
|
15
|
+
captureConsoleErrors?: boolean;
|
|
16
|
+
captureUnhandledRejections?: boolean;
|
|
17
|
+
maxBreadcrumbs?: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface Breadcrumb {
|
|
21
|
+
type: string;
|
|
22
|
+
message: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface ErrorPayload {
|
|
27
|
+
token: string;
|
|
28
|
+
message: string;
|
|
29
|
+
stacktrace?: string;
|
|
30
|
+
url: string;
|
|
31
|
+
level: string;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
networkStatus?: string;
|
|
34
|
+
breadcrumbs: Breadcrumb[];
|
|
35
|
+
timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare class HealCode {
|
|
39
|
+
private config;
|
|
40
|
+
private breadcrumbs;
|
|
41
|
+
private sentHashes;
|
|
42
|
+
private initialized;
|
|
43
|
+
constructor(config: HealCodeConfig);
|
|
44
|
+
private init;
|
|
45
|
+
private setupNetworkListeners;
|
|
46
|
+
private setupInteractionListeners;
|
|
47
|
+
private interceptConsole;
|
|
48
|
+
private interceptWindowErrors;
|
|
49
|
+
private interceptUnhandledRejections;
|
|
50
|
+
addBreadcrumb(type: string, message: string, data?: Record<string, unknown>): void;
|
|
51
|
+
capture(level: string, message: string, stacktrace?: string): void;
|
|
52
|
+
private generateHash;
|
|
53
|
+
private send;
|
|
54
|
+
setEnabled(enabled: boolean): void;
|
|
55
|
+
isEnabled(): boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare function loadConfig(configPath?: string): HealCodeFileConfig | null;
|
|
59
|
+
declare function initFromConfig(configPath?: string): HealCode | null;
|
|
60
|
+
|
|
61
|
+
export { HealCode, type HealCodeConfig, type HealCodeFileConfig, initFromConfig, loadConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
interface HealCodeConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
captureConsoleErrors?: boolean;
|
|
6
|
+
captureUnhandledRejections?: boolean;
|
|
7
|
+
maxBreadcrumbs?: number;
|
|
8
|
+
beforeSend?: (payload: ErrorPayload) => ErrorPayload | null;
|
|
9
|
+
}
|
|
10
|
+
interface HealCodeFileConfig {
|
|
11
|
+
token: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
options?: {
|
|
15
|
+
captureConsoleErrors?: boolean;
|
|
16
|
+
captureUnhandledRejections?: boolean;
|
|
17
|
+
maxBreadcrumbs?: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface Breadcrumb {
|
|
21
|
+
type: string;
|
|
22
|
+
message: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface ErrorPayload {
|
|
27
|
+
token: string;
|
|
28
|
+
message: string;
|
|
29
|
+
stacktrace?: string;
|
|
30
|
+
url: string;
|
|
31
|
+
level: string;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
networkStatus?: string;
|
|
34
|
+
breadcrumbs: Breadcrumb[];
|
|
35
|
+
timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare class HealCode {
|
|
39
|
+
private config;
|
|
40
|
+
private breadcrumbs;
|
|
41
|
+
private sentHashes;
|
|
42
|
+
private initialized;
|
|
43
|
+
constructor(config: HealCodeConfig);
|
|
44
|
+
private init;
|
|
45
|
+
private setupNetworkListeners;
|
|
46
|
+
private setupInteractionListeners;
|
|
47
|
+
private interceptConsole;
|
|
48
|
+
private interceptWindowErrors;
|
|
49
|
+
private interceptUnhandledRejections;
|
|
50
|
+
addBreadcrumb(type: string, message: string, data?: Record<string, unknown>): void;
|
|
51
|
+
capture(level: string, message: string, stacktrace?: string): void;
|
|
52
|
+
private generateHash;
|
|
53
|
+
private send;
|
|
54
|
+
setEnabled(enabled: boolean): void;
|
|
55
|
+
isEnabled(): boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare function loadConfig(configPath?: string): HealCodeFileConfig | null;
|
|
59
|
+
declare function initFromConfig(configPath?: string): HealCode | null;
|
|
60
|
+
|
|
61
|
+
export { HealCode, type HealCodeConfig, type HealCodeFileConfig, initFromConfig, loadConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
'use strict';const a0_0x5a3a5c=a0_0x3829;(function(_0x2dc44e,_0x4c947a){const _0x17031c=a0_0x3829,_0x4e81f0=_0x2dc44e();while(!![]){try{const _0x542e04=parseInt(_0x17031c(0x14c))/0x1+parseInt(_0x17031c(0x126))/0x2*(parseInt(_0x17031c(0xd9))/0x3)+-parseInt(_0x17031c(0x15d))/0x4*(-parseInt(_0x17031c(0x13f))/0x5)+parseInt(_0x17031c(0x144))/0x6+parseInt(_0x17031c(0x150))/0x7+-parseInt(_0x17031c(0x15a))/0x8+parseInt(_0x17031c(0x112))/0x9*(-parseInt(_0x17031c(0x102))/0xa);if(_0x542e04===_0x4c947a)break;else _0x4e81f0['push'](_0x4e81f0['shift']());}catch(_0x57d922){_0x4e81f0['push'](_0x4e81f0['shift']());}}}(a0_0x3c52,0x640c4));var C=Object[a0_0x5a3a5c(0x13a)],l=Object['defineProperty'],m=Object[a0_0x5a3a5c(0x100)],b=Object[a0_0x5a3a5c(0x131)],w=Object[a0_0x5a3a5c(0xda)],H=Object[a0_0x5a3a5c(0x12c)][a0_0x5a3a5c(0x121)],v=(_0x5bfce9,_0xe03a1e)=>{const _0x516a12=a0_0x5a3a5c,_0x352e23={'ZjBSJ':function(_0xd82f5,_0x48098b,_0x1d3a07,_0x35d285){return _0xd82f5(_0x48098b,_0x1d3a07,_0x35d285);}};for(var _0x3e7fd8 in _0xe03a1e)_0x352e23[_0x516a12(0x105)](l,_0x5bfce9,_0x3e7fd8,{'get':_0xe03a1e[_0x3e7fd8],'enumerable':!0x0});},u=(_0x514493,_0x2f346e,_0x47b9de,_0x5d0e0a)=>{const _0x139804=a0_0x5a3a5c,_0x4543e5={'pKitS':function(_0x2b6216,_0x1834fd){return _0x2b6216==_0x1834fd;},'LlEXo':_0x139804(0x167),'YXTPl':'function','RjppJ':function(_0x1dfe2b,_0x5764ae){return _0x1dfe2b(_0x5764ae);},'Hbues':function(_0x3d4553,_0x283bbe){return _0x3d4553!==_0x283bbe;},'cOPZv':function(_0x42b4dd,_0x5cdd8b,_0x308db8,_0x45b812){return _0x42b4dd(_0x5cdd8b,_0x308db8,_0x45b812);},'eHKuA':function(_0x1746e7,_0x218e00,_0x4fe2d1){return _0x1746e7(_0x218e00,_0x4fe2d1);}};if(_0x2f346e&&_0x4543e5[_0x139804(0x11c)](typeof _0x2f346e,_0x4543e5[_0x139804(0x119)])||_0x4543e5[_0x139804(0x11c)](typeof _0x2f346e,_0x4543e5[_0x139804(0x158)])){for(let _0xa418c8 of _0x4543e5[_0x139804(0xea)](b,_0x2f346e))!H[_0x139804(0xe7)](_0x514493,_0xa418c8)&&_0x4543e5[_0x139804(0x155)](_0xa418c8,_0x47b9de)&&_0x4543e5[_0x139804(0xe9)](l,_0x514493,_0xa418c8,{'get':()=>_0x2f346e[_0xa418c8],'enumerable':!(_0x5d0e0a=_0x4543e5[_0x139804(0x11e)](m,_0x2f346e,_0xa418c8))||_0x5d0e0a[_0x139804(0x146)]});}return _0x514493;},f=(_0x22ad50,_0x239689,_0x33053e)=>(_0x33053e=_0x22ad50!=null?C(w(_0x22ad50)):{},u(_0x239689||!_0x22ad50||!_0x22ad50[a0_0x5a3a5c(0x13b)]?l(_0x33053e,'default',{'value':_0x22ad50,'enumerable':!0x0}):_0x33053e,_0x22ad50)),y=_0x18e047=>u(l({},a0_0x5a3a5c(0x13b),{'value':!0x0}),_0x18e047),N={};function a0_0x3829(_0x5ebf82,_0x1e21d7){_0x5ebf82=_0x5ebf82-0xd7;const _0x3c52cf=a0_0x3c52();let _0x3829a3=_0x3c52cf[_0x5ebf82];if(a0_0x3829['DpnGLE']===undefined){var _0x2a5301=function(_0xa9eef5){const _0x59f0a6='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x380c9a='',_0x2ff39b='';for(let _0x1ba2ee=0x0,_0x54eecb,_0x5ec5ce,_0x2a6759=0x0;_0x5ec5ce=_0xa9eef5['charAt'](_0x2a6759++);~_0x5ec5ce&&(_0x54eecb=_0x1ba2ee%0x4?_0x54eecb*0x40+_0x5ec5ce:_0x5ec5ce,_0x1ba2ee++%0x4)?_0x380c9a+=String['fromCharCode'](0xff&_0x54eecb>>(-0x2*_0x1ba2ee&0x6)):0x0){_0x5ec5ce=_0x59f0a6['indexOf'](_0x5ec5ce);}for(let _0x50a02d=0x0,_0x318128=_0x380c9a['length'];_0x50a02d<_0x318128;_0x50a02d++){_0x2ff39b+='%'+('00'+_0x380c9a['charCodeAt'](_0x50a02d)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x2ff39b);};a0_0x3829['lJLHGL']=_0x2a5301,a0_0x3829['KNiQkT']={},a0_0x3829['DpnGLE']=!![];}const _0x576947=_0x3c52cf[0x0],_0x3b0fe2=_0x5ebf82+_0x576947,_0x185caf=a0_0x3829['KNiQkT'][_0x3b0fe2];return!_0x185caf?(_0x3829a3=a0_0x3829['lJLHGL'](_0x3829a3),a0_0x3829['KNiQkT'][_0x3b0fe2]=_0x3829a3):_0x3829a3=_0x185caf,_0x3829a3;}v(N,{'HealCode':()=>s,'initFromConfig':()=>h,'loadConfig':()=>p}),module['exports']=y(N);var g=a0_0x5a3a5c(0x139),E=g+'/api/logs/',x=0x14,s=class{constructor(_0x5879a8){const _0x13fcff=a0_0x5a3a5c,_0x46a00c={'fXBQU':function(_0x84db9a,_0x1842cf){return _0x84db9a!==_0x1842cf;},'vZjPB':function(_0x21e8c3,_0x29a0b5){return _0x21e8c3!==_0x29a0b5;},'aNXyR':function(_0x336b7e,_0x4bef67){return _0x336b7e<_0x4bef67;}};this[_0x13fcff(0xf3)]=[],this[_0x13fcff(0xf7)]=new Set(),this[_0x13fcff(0x16d)]=!0x1,(this['config']={'token':_0x5879a8[_0x13fcff(0x15b)],'endpoint':_0x5879a8['endpoint']||E,'enabled':_0x46a00c[_0x13fcff(0x16c)](_0x5879a8[_0x13fcff(0x113)],!0x1),'captureConsoleErrors':_0x5879a8[_0x13fcff(0x154)]!==!0x1,'captureUnhandledRejections':_0x46a00c[_0x13fcff(0x14e)](_0x5879a8[_0x13fcff(0xeb)],!0x1),'maxBreadcrumbs':_0x5879a8[_0x13fcff(0xf9)]||x,'beforeSend':_0x5879a8[_0x13fcff(0xff)]},this[_0x13fcff(0x110)][_0x13fcff(0x113)]&&_0x46a00c[_0x13fcff(0x13d)](typeof window,'u')&&this['init']());}[a0_0x5a3a5c(0x11f)](){const _0x1125a5=a0_0x5a3a5c,_0x1c0310={'UFEvc':'[HealCode]\x20Initialized\x20successfully'};this[_0x1125a5(0x16d)]||(this[_0x1125a5(0x16d)]=!0x0,this[_0x1125a5(0xee)](),this[_0x1125a5(0x117)](),this[_0x1125a5(0x110)]['captureConsoleErrors']&&this[_0x1125a5(0x101)](),this['interceptWindowErrors'](),this[_0x1125a5(0x110)]['captureUnhandledRejections']&&this['interceptUnhandledRejections'](),console[_0x1125a5(0x120)](_0x1c0310['UFEvc']));}[a0_0x5a3a5c(0xee)](){const _0x30ddcb=a0_0x5a3a5c,_0x31ac45={'tYpKS':_0x30ddcb(0x14b),'PUOcw':_0x30ddcb(0x133)};window['addEventListener'](_0x31ac45['tYpKS'],()=>this[_0x30ddcb(0x106)](_0x30ddcb(0x104),'Connection\x20restored',{'status':_0x30ddcb(0x14b)})),window[_0x30ddcb(0x161)](_0x31ac45['PUOcw'],()=>this['addBreadcrumb'](_0x30ddcb(0x104),_0x30ddcb(0x157),{'status':_0x30ddcb(0x133)}));}[a0_0x5a3a5c(0x117)](){const _0x2e7dc3=a0_0x5a3a5c,_0x380f24={'liVlg':function(_0x154bac,_0x173130){return _0x154bac||_0x173130;},'QOUru':function(_0x5bed50,_0x4ba273){return _0x5bed50<_0x4ba273;},'IipEi':function(_0x3f45dd,_0x43a361){return _0x3f45dd-_0x43a361;},'PjVix':function(_0x4d10f4,_0x4c641a){return _0x4d10f4<<_0x4c641a;},'NariC':function(_0x1fc8ed,_0x13741f){return _0x1fc8ed&_0x13741f;},'ArNiD':function(_0x1d3789,_0x5dd72f){return _0x1d3789!==_0x5dd72f;},'jmtSL':'TUFfw','OmMNG':_0x2e7dc3(0xfd),'SPuEB':'click'};document[_0x2e7dc3(0x161)](_0x380f24[_0x2e7dc3(0x165)],_0x82432d=>{const _0x1c20a3=_0x2e7dc3;if(_0x380f24[_0x1c20a3(0x10a)](_0x380f24[_0x1c20a3(0x130)],_0x1c20a3(0x160))){let _0x56cc6d=_0x82432d[_0x1c20a3(0x15c)];this[_0x1c20a3(0x106)](_0x380f24[_0x1c20a3(0xf1)],_0x1c20a3(0xe3)+_0x56cc6d[_0x1c20a3(0xd7)],{'id':_0x56cc6d['id']||void 0x0,'className':_0x56cc6d['className']||void 0x0,'text':_0x56cc6d[_0x1c20a3(0xd8)]?.[_0x1c20a3(0x142)](0x0,0x32)||void 0x0});}else{let _0x23b565=_0x53b8ef+_0x380f24[_0x1c20a3(0x114)](_0x28d233,''),_0x1045c2=0x0;for(let _0x56c20e=0x0;_0x380f24[_0x1c20a3(0xe0)](_0x56c20e,_0x23b565[_0x1c20a3(0x132)]);_0x56c20e++){let _0x2a1270=_0x23b565[_0x1c20a3(0xe5)](_0x56c20e);_0x1045c2=_0x380f24['IipEi'](_0x380f24[_0x1c20a3(0x11d)](_0x1045c2,0x5),_0x1045c2)+_0x2a1270,_0x1045c2=_0x380f24[_0x1c20a3(0xe2)](_0x1045c2,_0x1045c2);}return _0x1045c2[_0x1c20a3(0xdb)]();}},!0x0);}[a0_0x5a3a5c(0x101)](){const _0x387059=a0_0x5a3a5c,_0x550e19={'xemSj':function(_0x5b54d7,_0x39b97a){return _0x5b54d7==_0x39b97a;},'pClzE':'string','hNhxO':_0x387059(0xef),'tehpQ':_0x387059(0xe8),'FboXt':function(_0x48b9fe,_0x3a74a9){return _0x48b9fe!==_0x3a74a9;},'CfhnF':_0x387059(0x143),'SHhHK':'HfGdH'};let _0x2cfa05=console[_0x387059(0xe8)];console[_0x387059(0xe8)]=(..._0x34a3f4)=>{const _0x4d7e7b=_0x387059,_0x3f45d2={'ShDtI':function(_0x39e8ca,_0x5271c7){const _0x586a6d=a0_0x3829;return _0x550e19[_0x586a6d(0x13c)](_0x39e8ca,_0x5271c7);},'uKwtj':_0x550e19[_0x4d7e7b(0x10e)],'yDPeg':_0x550e19[_0x4d7e7b(0x163)],'tuNEd':_0x550e19[_0x4d7e7b(0xfe)]};if(_0x550e19['FboXt'](_0x550e19[_0x4d7e7b(0x140)],_0x550e19['SHhHK'])){let _0x1cc978=_0x34a3f4[_0x4d7e7b(0x15e)](_0x4a2b0c=>typeof _0x4a2b0c==_0x4d7e7b(0x167)?JSON[_0x4d7e7b(0x10d)](_0x4a2b0c):String(_0x4a2b0c))[_0x4d7e7b(0xdd)]('\x20');this[_0x4d7e7b(0x152)](_0x550e19[_0x4d7e7b(0xfe)],_0x1cc978),_0x2cfa05['apply'](console,_0x34a3f4);}else _0x38093c['onerror']=(_0x4483f6,_0x563732,_0x565547,_0x528eed,_0x4f16b6)=>{const _0x32c5e5=_0x4d7e7b;let _0x2b29f8=_0x3f45d2[_0x32c5e5(0x107)](typeof _0x4483f6,_0x3f45d2[_0x32c5e5(0x128)])?_0x4483f6:_0x3f45d2[_0x32c5e5(0x135)];this['capture'](_0x3f45d2['tuNEd'],_0x2b29f8+'\x20at\x20'+_0x563732+':'+_0x565547+':'+_0x528eed,_0x4f16b6?.[_0x32c5e5(0x156)]);};};}[a0_0x5a3a5c(0x12f)](){const _0x5ee505=a0_0x5a3a5c,_0xd10e73={'kQzSM':function(_0x2c0871,_0x1c2f4a){return _0x2c0871==_0x1c2f4a;},'lRKCv':'string','lRayg':'Unknown\x20error','oqBns':_0x5ee505(0xe8)};window['onerror']=(_0x1760b9,_0x329ca2,_0x47dd2a,_0x5e2b0a,_0x3452f9)=>{const _0x3742e9=_0x5ee505;let _0x401d3a=_0xd10e73['kQzSM'](typeof _0x1760b9,_0xd10e73[_0x3742e9(0x13e)])?_0x1760b9:_0xd10e73[_0x3742e9(0x153)];this[_0x3742e9(0x152)](_0xd10e73[_0x3742e9(0x170)],_0x401d3a+_0x3742e9(0x14d)+_0x329ca2+':'+_0x47dd2a+':'+_0x5e2b0a,_0x3452f9?.[_0x3742e9(0x156)]);};}[a0_0x5a3a5c(0x16e)](){const _0x4fc200=a0_0x5a3a5c,_0x8f1f33={'wkqAS':'error'};window[_0x4fc200(0xe6)]=_0x7b4fb8=>{const _0x568be8=_0x4fc200;this['capture'](_0x8f1f33[_0x568be8(0x11b)],'Unhandled\x20Rejection:\x20'+_0x7b4fb8[_0x568be8(0x12b)]);};}[a0_0x5a3a5c(0x106)](_0x167bf3,_0x3a28a2,_0x97c40a){const _0x5c2ab7=a0_0x5a3a5c;let _0x1a045d={'type':_0x167bf3,'message':_0x3a28a2,'timestamp':new Date()[_0x5c2ab7(0xfb)](),'data':_0x97c40a};this[_0x5c2ab7(0xf3)][_0x5c2ab7(0x141)](_0x1a045d),this[_0x5c2ab7(0xf3)][_0x5c2ab7(0x132)]>this[_0x5c2ab7(0x110)][_0x5c2ab7(0xf9)]&&this[_0x5c2ab7(0xf3)][_0x5c2ab7(0x10f)]();}[a0_0x5a3a5c(0x152)](_0x4752ad,_0x3495c5,_0x116ead){const _0x1e362a=a0_0x5a3a5c,_0x450114={'vKgca':function(_0xbad84c,_0x29170b,_0x144635){return _0xbad84c(_0x29170b,_0x144635);},'ClmOZ':function(_0x47f895,_0x1f29d4){return _0x47f895*_0x1f29d4;},'FQtYj':_0x1e362a(0x14b),'grmiA':_0x1e362a(0x133)};if(!this['config'][_0x1e362a(0x113)])return;let _0x33095c=this[_0x1e362a(0xf8)](_0x3495c5,_0x116ead);if(this[_0x1e362a(0xf7)][_0x1e362a(0x116)](_0x33095c))return;this['sentHashes']['add'](_0x33095c),_0x450114[_0x1e362a(0x136)](setTimeout,()=>this[_0x1e362a(0xf7)][_0x1e362a(0xf4)](_0x33095c),_0x450114[_0x1e362a(0x169)](0x12c,0x3e8));let _0x1c81e6={'token':this[_0x1e362a(0x110)]['token'],'message':_0x3495c5,'stacktrace':_0x116ead,'url':window['location'][_0x1e362a(0xf0)],'level':_0x4752ad,'userAgent':navigator[_0x1e362a(0x12d)],'networkStatus':navigator[_0x1e362a(0x16f)]?_0x450114[_0x1e362a(0x10b)]:_0x450114[_0x1e362a(0x108)],'breadcrumbs':[...this[_0x1e362a(0xf3)]],'timestamp':new Date()[_0x1e362a(0xfb)]()};this[_0x1e362a(0x110)][_0x1e362a(0xff)]&&(_0x1c81e6=this[_0x1e362a(0x110)][_0x1e362a(0xff)](_0x1c81e6),!_0x1c81e6)||this['send'](_0x1c81e6);}[a0_0x5a3a5c(0xf8)](_0x224240,_0x26c682){const _0x5093de=a0_0x5a3a5c,_0x61298e={'DMyBp':function(_0x3e2aca,_0x188fa4){return _0x3e2aca+_0x188fa4;},'XrMRw':function(_0x41adaa,_0x2d5692){return _0x41adaa<_0x2d5692;},'NSoJV':function(_0x45329f,_0x15ee9d){return _0x45329f-_0x15ee9d;},'TQogP':function(_0xe88c22,_0x263e76){return _0xe88c22<<_0x263e76;},'HwWSw':function(_0x8efbd3,_0x4c039b){return _0x8efbd3&_0x4c039b;}};let _0x5990d3=_0x61298e[_0x5093de(0x11a)](_0x224240,_0x26c682||''),_0x86820c=0x0;for(let _0x85128b=0x0;_0x61298e[_0x5093de(0x122)](_0x85128b,_0x5990d3[_0x5093de(0x132)]);_0x85128b++){let _0x1d180e=_0x5990d3[_0x5093de(0xe5)](_0x85128b);_0x86820c=_0x61298e[_0x5093de(0x11a)](_0x61298e[_0x5093de(0x137)](_0x61298e[_0x5093de(0xf2)](_0x86820c,0x5),_0x86820c),_0x1d180e),_0x86820c=_0x61298e['HwWSw'](_0x86820c,_0x86820c);}return _0x86820c[_0x5093de(0xdb)]();}async['send'](_0x402ec4){const _0x4534fd=a0_0x5a3a5c,_0x5128d2={'sRDdX':_0x4534fd(0xf6),'MLSmw':function(_0x47cfbf,_0x3b0e8a){return _0x47cfbf+_0x3b0e8a;},'GgYch':_0x4534fd(0x115),'qbaGs':_0x4534fd(0x10c),'bMoeO':_0x4534fd(0x147),'cVBYJ':_0x4534fd(0xdf),'CwZjn':'[HealCode]\x20Failed\x20to\x20report\x20error:','mdAdz':_0x4534fd(0x16b)};try{let _0x1d8e3e=this[_0x4534fd(0x110)][_0x4534fd(0x164)];!_0x1d8e3e['endsWith'](_0x4534fd(0x115))&&!_0x1d8e3e[_0x4534fd(0x159)](_0x5128d2['sRDdX'])&&(_0x1d8e3e=_0x5128d2[_0x4534fd(0xec)](_0x1d8e3e[_0x4534fd(0xf5)](/\/$/,''),_0x5128d2['GgYch']));let _0x3ec29f=await fetch(_0x1d8e3e,{'method':_0x5128d2['qbaGs'],'headers':{'Content-Type':_0x5128d2[_0x4534fd(0xe1)],'X-HealCode-Token':this['config']['token']},'body':JSON[_0x4534fd(0x10d)](_0x402ec4),'keepalive':!0x0});_0x3ec29f['ok']?console[_0x4534fd(0x120)](_0x5128d2[_0x4534fd(0x151)]):console[_0x4534fd(0xde)](_0x5128d2[_0x4534fd(0x14a)],_0x3ec29f['status']);}catch(_0x3dc163){console['warn'](_0x5128d2[_0x4534fd(0x127)],_0x3dc163);}}[a0_0x5a3a5c(0x162)](_0x1ae36f){const _0x40d699=a0_0x5a3a5c;this[_0x40d699(0x110)][_0x40d699(0x113)]=_0x1ae36f;}[a0_0x5a3a5c(0x138)](){const _0x227ede=a0_0x5a3a5c;return this[_0x227ede(0x110)][_0x227ede(0x113)];}},a=f(require('fs')),d=f(require(a0_0x5a3a5c(0x14f))),k=a0_0x5a3a5c(0xfc);function S(_0x4dc455=process[a0_0x5a3a5c(0xe4)]()){const _0x4a181c=a0_0x5a3a5c,_0x58bf5f={'OJqtK':function(_0x4ac62c,_0x2b0778){return _0x4ac62c==_0x2b0778;},'rNQtc':_0x4a181c(0x167),'QASKS':_0x4a181c(0x125),'RgQwz':function(_0x3eae5c,_0x56b159){return _0x3eae5c(_0x56b159);},'ygRJT':function(_0x1aaee1,_0x5b9270){return _0x1aaee1!==_0x5b9270;},'uwuBL':function(_0x214907,_0x112ee6,_0x1c7e29,_0x5e8dfe){return _0x214907(_0x112ee6,_0x1c7e29,_0x5e8dfe);},'ZCMRp':function(_0x6f0fe,_0x2c00d0,_0x3f6442){return _0x6f0fe(_0x2c00d0,_0x3f6442);},'kYVVX':function(_0x33e40,_0x5c1d40){return _0x33e40!==_0x5c1d40;},'pvUEl':function(_0x20e351,_0x269a54){return _0x20e351===_0x269a54;},'qWQQp':_0x4a181c(0x166)};let _0x585b0b=_0x4dc455;for(;_0x58bf5f[_0x4a181c(0x124)](_0x585b0b,d[_0x4a181c(0x148)](_0x585b0b));){if(_0x58bf5f['pvUEl'](_0x4a181c(0x103),_0x58bf5f[_0x4a181c(0x145)])){if(_0x3fd5b5&&_0x58bf5f[_0x4a181c(0x129)](typeof _0x3814e2,_0x58bf5f[_0x4a181c(0x134)])||_0x58bf5f[_0x4a181c(0x129)](typeof _0xec89cc,_0x58bf5f[_0x4a181c(0xdc)])){for(let _0x3b5f52 of _0x58bf5f[_0x4a181c(0x109)](_0x5a696e,_0x356061))!_0x117186['call'](_0x158022,_0x3b5f52)&&_0x58bf5f[_0x4a181c(0xed)](_0x3b5f52,_0x420aec)&&_0x58bf5f[_0x4a181c(0x118)](_0x284f17,_0x28683d,_0x3b5f52,{'get':()=>_0x781b3d[_0x3b5f52],'enumerable':!(_0x56c67e=_0x58bf5f[_0x4a181c(0x171)](_0x15a326,_0xd04a86,_0x3b5f52))||_0x278b99[_0x4a181c(0x146)]});}return _0x553dcd;}else{let _0x2049d8=d['join'](_0x585b0b,k);if(a[_0x4a181c(0x16a)](_0x2049d8))return _0x2049d8;_0x585b0b=d[_0x4a181c(0x148)](_0x585b0b);}}return null;}function p(_0x1d1d16){const _0x19e0b6=a0_0x5a3a5c,_0x3c21d2={'lXaWy':function(_0x1e55ed){return _0x1e55ed();},'tCWUb':_0x19e0b6(0x12e)};let _0x2749fe=_0x1d1d16||_0x3c21d2[_0x19e0b6(0x12a)](S);if(!_0x2749fe||!a[_0x19e0b6(0x16a)](_0x2749fe))return null;try{let _0x34b15e=a[_0x19e0b6(0x111)](_0x2749fe,'utf-8');return JSON['parse'](_0x34b15e);}catch(_0x4559a0){return console[_0x19e0b6(0xe8)](_0x3c21d2[_0x19e0b6(0x123)],_0x4559a0),null;}}function h(_0x2bcc1e){const _0xeb8563=a0_0x5a3a5c,_0x2643a7={'IFajV':function(_0x59edca,_0x2d96b7){return _0x59edca(_0x2d96b7);},'qaIrv':'[HealCode]\x20No\x20config\x20file\x20found.\x20Run\x20`npx\x20healcode\x20init`\x20to\x20set\x20up.'};let _0x2ff312=_0x2643a7[_0xeb8563(0x168)](p,_0x2bcc1e);if(!_0x2ff312)return console[_0xeb8563(0xde)](_0x2643a7[_0xeb8563(0xfa)]),null;let _0x3daec5={'token':_0x2ff312[_0xeb8563(0x15b)],'endpoint':_0x2ff312[_0xeb8563(0x164)],'enabled':_0x2ff312[_0xeb8563(0x113)],'captureConsoleErrors':_0x2ff312[_0xeb8563(0x15f)]?.[_0xeb8563(0x154)],'captureUnhandledRejections':_0x2ff312[_0xeb8563(0x15f)]?.[_0xeb8563(0xeb)],'maxBreadcrumbs':_0x2ff312[_0xeb8563(0x15f)]?.['maxBreadcrumbs']};return new s(_0x3daec5);}0x0&&(module[a0_0x5a3a5c(0x149)]={'HealCode':HealCode,'initFromConfig':initFromConfig,'loadConfig':loadConfig});function a0_0x3c52(){const _0x51f53a=['y2fWDhvYzunVBNnVBgvfCNjVCNm','sgj1zxm','C3rHy2S','q29UBMvJDgLVBIbSB3n0','wvHuugW','zw5KC1DPDgG','ntKZmZGXnMzLEvfPBa','Dg9Rzw4','DgfYz2v0','mJCWohLyq29XDW','BwfW','B3b0Aw9UCW','q0fjBvu','ywrKrxzLBNrmAxn0zw5LCG','C2v0rw5HyMXLza','Ae5OEe8','zw5KCg9PBNq','u1b1rui','rxvjBKq','B2jQzwn0','suzHALy','q2XTt1O','zxHPC3rZu3LUyW','w0HLywXdB2rLxsbozxr3B3jRigvYCM9YoG','zLHcuvu','Aw5PDgLHBgL6zwq','Aw50zxjJzxb0vw5Oyw5KBgvKuMvQzwn0Aw9UCW','B25mAw5L','B3fcBNm','wKnnuNa','DgfNtMfTzq','Aw5UzxjuzxH0','mJm0ode0mMLdEKn0yW','z2v0uhjVDg90ExbLt2y','Dg9tDhjPBMC','uufts1m','AM9PBG','D2fYBG','w0HLywXdB2rLxsbfCNjVCIbYzxbVCNrLzcbZDwnJzxnZzNvSBhK','uu9vCNu','yK1Vzu8','tMfYAum','q2XPy2TLzcbVBIa','y3DK','y2HHCKnVzgvbDa','B251BMHHBMrSzwrYzwPLy3rPB24','y2fSBa','zxjYB3i','y09qwNy','uMPWCeO','y2fWDhvYzvvUAgfUzgXLzfjLAMvJDgLVBNm','tuXtBxC','EwDssLq','C2v0Dxbozxr3B3jRtgLZDgvUzxjZ','vw5RBM93BIbLCNjVCG','AhjLzG','t21ntKC','vffVz1a','yNjLywrJCNvTyNm','zgvSzxrL','CMvWBgfJzq','l2fWAs9SB2DZ','C2vUDeHHC2HLCW','z2vUzxjHDgviyxnO','Bwf4qNjLywrJCNvTyNm','CwfjCNy','Dg9ju09tDhjPBMC','AgvHBgnVzguUy29UzMLNlMPZB24','DwKUy2XPy2S','DgvOCfe','yMvMB3jLu2vUza','z2v0t3DUuhjVCgvYDhLezxnJCMLWDg9Y','Aw50zxjJzxb0q29UC29Szq','mJbys1rHBe8','wK9LDeK','BMv0D29YAW','wMPcu0O','ywrKqNjLywrJCNvTyG','u2HeDeK','z3jTAue','uMDrD3O','qxjoAuq','rLf0wwO','ue9tva','C3rYAw5NAwz5','CenSEKu','C2HPzNq','y29UzMLN','CMvHzezPBgvtEw5J','mZyXmdu1n3Dmr3LvsW','zw5HyMXLza','BgLwBgC','l2fWAs9SB2DZlW','AgfZ','C2v0DxbjBNrLCMfJDgLVBKXPC3rLBMvYCW','DxD1qKW','tgXfwg8','re15qNa','D2TXqvm','CeTPDfm','ugPwAxG','zuHlDue','Aw5PDa','Bg9N','AgfZt3DUuhjVCgvYDhK','whjnuNC','Denxvwi','A1LwvLG','zNvUy3rPB24','mLfcu3fdAq','BwrbzhO','DuT3DgO','t0PXDeS','BfHHv3K','CMvHC29U','ChjVDg90ExbL','DxnLCKfNzw50','w0HLywXdB2rLxsbgywLSzwqGDg8GBg9HzcbJB25MAwC6','Aw50zxjJzxb0v2LUzg93rxjYB3jZ','AM10u0W','z2v0t3DUuhjVCgvYDhLoyw1LCW','BgvUz3rO','B2zMBgLUzq','CK5rDgm','EurqzwC','DKTNy2e','tLnVsLy','AxnfBMfIBgvK','Ahr0Chm6lY9WCMvWywLKlxrYAwjHBc1PBNzPDgvKlwvUzY50CNLJBg91zgzSyxjLlMnVBq','y3jLyxrL','x19LC01VzhvSzq','EgvTu2O','yu5yEvi','Bfjlq3y','mJK2nuz2vMXgsa','q2zOBKy','ChvZAa','C3vIC3rYAw5N','tNHzuM4','mtm5mZmYme5tDKjWuW','CvDruxa','zw51BwvYywjSzq','yxbWBgLJyxrPB24VANnVBG','zgLYBMfTzq','zxHWB3j0CW','q3DAAM4','B25SAw5L','nte5mJy4A29pEuLs','igf0ia','DLPQuei','Cgf0Aa','mti3ndqYvNr2tLvu','y1zcwuO','y2fWDhvYzq','BfjHEwC'];a0_0x3c52=function(){return _0x51f53a;};return a0_0x3c52();}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var c="https://prepaid-tribal-invited-eng.trycloudflare.com";var u=c+"/api/logs/",f=20,a=class{constructor(e){this.breadcrumbs=[];this.sentHashes=new Set;this.initialized=!1;this.config={token:e.token,endpoint:e.endpoint||u,enabled:e.enabled!==!1,captureConsoleErrors:e.captureConsoleErrors!==!1,captureUnhandledRejections:e.captureUnhandledRejections!==!1,maxBreadcrumbs:e.maxBreadcrumbs||f,beforeSend:e.beforeSend},this.config.enabled&&typeof window<"u"&&this.init()}init(){this.initialized||(this.initialized=!0,this.setupNetworkListeners(),this.setupInteractionListeners(),this.config.captureConsoleErrors&&this.interceptConsole(),this.interceptWindowErrors(),this.config.captureUnhandledRejections&&this.interceptUnhandledRejections(),console.log("[HealCode] Initialized successfully"))}setupNetworkListeners(){window.addEventListener("online",()=>this.addBreadcrumb("network","Connection restored",{status:"online"})),window.addEventListener("offline",()=>this.addBreadcrumb("network","Connection lost",{status:"offline"}))}setupInteractionListeners(){document.addEventListener("click",e=>{let n=e.target;this.addBreadcrumb("ui.click",`Clicked on ${n.tagName}`,{id:n.id||void 0,className:n.className||void 0,text:n.innerText?.substring(0,50)||void 0})},!0)}interceptConsole(){let e=console.error;console.error=(...n)=>{let o=n.map(t=>typeof t=="object"?JSON.stringify(t):String(t)).join(" ");this.capture("error",o),e.apply(console,n)}}interceptWindowErrors(){window.onerror=(e,n,o,t,r)=>{let l=typeof e=="string"?e:"Unknown error";this.capture("error",`${l} at ${n}:${o}:${t}`,r?.stack)}}interceptUnhandledRejections(){window.onunhandledrejection=e=>{this.capture("error",`Unhandled Rejection: ${e.reason}`)}}addBreadcrumb(e,n,o){let t={type:e,message:n,timestamp:new Date().toISOString(),data:o};this.breadcrumbs.push(t),this.breadcrumbs.length>this.config.maxBreadcrumbs&&this.breadcrumbs.shift()}capture(e,n,o){if(!this.config.enabled)return;let t=this.generateHash(n,o);if(this.sentHashes.has(t))return;this.sentHashes.add(t),setTimeout(()=>this.sentHashes.delete(t),300*1e3);let r={token:this.config.token,message:n,stacktrace:o,url:window.location.href,level:e,userAgent:navigator.userAgent,networkStatus:navigator.onLine?"online":"offline",breadcrumbs:[...this.breadcrumbs],timestamp:new Date().toISOString()};this.config.beforeSend&&(r=this.config.beforeSend(r),!r)||this.send(r)}generateHash(e,n){let o=e+(n||""),t=0;for(let r=0;r<o.length;r++){let l=o.charCodeAt(r);t=(t<<5)-t+l,t=t&t}return t.toString()}async send(e){try{let n=this.config.endpoint;!n.endsWith("/api/logs/")&&!n.endsWith("/api/logs")&&(n=n.replace(/\/$/,"")+"/api/logs/");let o=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json","X-HealCode-Token":this.config.token},body:JSON.stringify(e),keepalive:!0});o.ok?console.log("[HealCode] Error reported successfully"):console.warn("[HealCode] Failed to report error:",o.status)}catch(n){console.warn("[HealCode] Network error:",n)}}setEnabled(e){this.config.enabled=e}isEnabled(){return this.config.enabled}};import*as s from"fs";import*as d from"path";var g="healcode.config.json";function h(i=process.cwd()){let e=i;for(;e!==d.dirname(e);){let n=d.join(e,g);if(s.existsSync(n))return n;e=d.dirname(e)}return null}function p(i){let e=i||h();if(!e||!s.existsSync(e))return null;try{let n=s.readFileSync(e,"utf-8");return JSON.parse(n)}catch(n){return console.error("[HealCode] Failed to load config:",n),null}}function C(i){let e=p(i);if(!e)return console.warn("[HealCode] No config file found. Run `npx healcode init` to set up."),null;let n={token:e.token,endpoint:e.endpoint,enabled:e.enabled,captureConsoleErrors:e.options?.captureConsoleErrors,captureUnhandledRejections:e.options?.captureUnhandledRejections,maxBreadcrumbs:e.options?.maxBreadcrumbs};return new a(n)}export{a as HealCode,C as initFromConfig,p as loadConfig};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "healcode-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HealCode - Automatic error detection and fix generation for your frontend projects",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"healcode": "./bin/healcode.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --minify && npm run obfuscate",
|
|
13
|
+
"obfuscate": "javascript-obfuscator dist/index.js --output dist/index.js --compact true --control-flow-flattening true --dead-code-injection true --string-array true --string-array-encoding base64",
|
|
14
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"postinstall": "node bin/postinstall.js || exit 0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"healcode",
|
|
20
|
+
"error-tracking",
|
|
21
|
+
"auto-fix",
|
|
22
|
+
"debugging",
|
|
23
|
+
"monitoring",
|
|
24
|
+
"frontend"
|
|
25
|
+
],
|
|
26
|
+
"author": "HealCode Team",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/healcode/client"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"bin",
|
|
35
|
+
"urls.config.json"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"commander": "^12.0.0",
|
|
40
|
+
"inquirer": "^9.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"javascript-obfuscator": "^4.1.1",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/urls.config.json
ADDED