@analyticscli/growth-engineer 0.1.0-preview.14 → 0.1.0-preview.18
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/dist/config.d.ts +775 -22
- package/dist/config.js +39 -5
- package/dist/config.js.map +1 -1
- package/dist/index.js +134 -21
- package/dist/index.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +1 -1
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/export-coolify-summary.d.mts +2 -0
- package/dist/runtime/export-coolify-summary.mjs +230 -0
- package/dist/runtime/export-coolify-summary.mjs.map +1 -0
- package/dist/runtime/export-paddle-summary.d.mts +2 -0
- package/dist/runtime/export-paddle-summary.mjs +170 -0
- package/dist/runtime/export-paddle-summary.mjs.map +1 -0
- package/dist/runtime/export-sentry-summary.mjs +265 -38
- package/dist/runtime/export-sentry-summary.mjs.map +1 -1
- package/dist/runtime/export-seo-summary.d.mts +2 -0
- package/dist/runtime/export-seo-summary.mjs +503 -0
- package/dist/runtime/export-seo-summary.mjs.map +1 -0
- package/dist/runtime/openclaw-exporters-lib.d.mts +50 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +761 -57
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
- package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-env.mjs +5 -0
- package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +399 -26
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +564 -69
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +150 -2
- package/dist/runtime/openclaw-growth-shared.mjs +489 -7
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +584 -48
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +82 -6
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +1501 -105
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -1
- package/templates/config.example.json +120 -71
|
@@ -12,6 +12,41 @@ const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
12
12
|
const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
|
|
13
13
|
const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
|
|
14
14
|
(process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
|
|
15
|
+
const ACCOUNT_SIGNAL_CONNECTORS = [
|
|
16
|
+
'stripe',
|
|
17
|
+
'lemonsqueezy',
|
|
18
|
+
'adapty',
|
|
19
|
+
'superwall',
|
|
20
|
+
'google-play',
|
|
21
|
+
'datadog',
|
|
22
|
+
'bugsnag',
|
|
23
|
+
'intercom',
|
|
24
|
+
'zendesk',
|
|
25
|
+
'apple-search-ads',
|
|
26
|
+
'google-ads',
|
|
27
|
+
'meta-ads',
|
|
28
|
+
'tiktok-ads',
|
|
29
|
+
'vercel',
|
|
30
|
+
'cloudflare',
|
|
31
|
+
'resend',
|
|
32
|
+
'customerio',
|
|
33
|
+
'mailchimp',
|
|
34
|
+
'appfollow',
|
|
35
|
+
'apptweak',
|
|
36
|
+
'linear',
|
|
37
|
+
'postiz',
|
|
38
|
+
];
|
|
39
|
+
const SUPPORTED_CONNECTORS = [
|
|
40
|
+
'analytics',
|
|
41
|
+
'github',
|
|
42
|
+
'asc',
|
|
43
|
+
'revenuecat',
|
|
44
|
+
'paddle',
|
|
45
|
+
'seo',
|
|
46
|
+
'sentry',
|
|
47
|
+
'coolify',
|
|
48
|
+
...ACCOUNT_SIGNAL_CONNECTORS,
|
|
49
|
+
];
|
|
15
50
|
function printHelpAndExit(exitCode, reason = null) {
|
|
16
51
|
if (reason) {
|
|
17
52
|
process.stderr.write(`${reason}\n\n`);
|
|
@@ -28,7 +63,7 @@ Options:
|
|
|
28
63
|
--config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
|
|
29
64
|
--test-connections Run live API/connector smoke checks for enabled channels
|
|
30
65
|
--only-connectors <list>
|
|
31
|
-
Limit live checks to
|
|
66
|
+
Limit live checks to ${SUPPORTED_CONNECTORS.join(',')}
|
|
32
67
|
--timeout-ms <ms> Connection test timeout in milliseconds (default: ${DEFAULT_CONNECTION_TIMEOUT_MS})
|
|
33
68
|
--progress-json Emit machine-readable progress events on stderr
|
|
34
69
|
--json Print JSON only (default)
|
|
@@ -99,8 +134,58 @@ function normalizeConnectorKey(value) {
|
|
|
99
134
|
return 'asc';
|
|
100
135
|
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
101
136
|
return 'revenuecat';
|
|
137
|
+
if (['paddle', 'paddle-billing', 'billing-metrics', 'web-revenue'].includes(normalized))
|
|
138
|
+
return 'paddle';
|
|
139
|
+
if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo', 'organic-search'].includes(normalized))
|
|
140
|
+
return 'seo';
|
|
102
141
|
if (['sentry', 'sentry-api', 'sentry-mcp', 'glitchtip', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
103
142
|
return 'sentry';
|
|
143
|
+
if (['coolify', 'coolify-api', 'deployment', 'deployments', 'hosting', 'infra', 'infrastructure'].includes(normalized))
|
|
144
|
+
return 'coolify';
|
|
145
|
+
if (['stripe', 'stripe-billing', 'stripe-payments'].includes(normalized))
|
|
146
|
+
return 'stripe';
|
|
147
|
+
if (['lemonsqueezy', 'lemon-squeezy', 'lemon', 'ls'].includes(normalized))
|
|
148
|
+
return 'lemonsqueezy';
|
|
149
|
+
if (['adapty', 'adapty-paywalls', 'adapty-subscriptions'].includes(normalized))
|
|
150
|
+
return 'adapty';
|
|
151
|
+
if (['superwall', 'superwall-paywalls'].includes(normalized))
|
|
152
|
+
return 'superwall';
|
|
153
|
+
if (['google-play', 'google-play-console', 'play-console', 'play-store', 'android-store'].includes(normalized))
|
|
154
|
+
return 'google-play';
|
|
155
|
+
if (['datadog', 'datadog-rum', 'datadog-apm', 'datadog-logs'].includes(normalized))
|
|
156
|
+
return 'datadog';
|
|
157
|
+
if (['bugsnag', 'bugsnag-crashes'].includes(normalized))
|
|
158
|
+
return 'bugsnag';
|
|
159
|
+
if (['intercom', 'intercom-support'].includes(normalized))
|
|
160
|
+
return 'intercom';
|
|
161
|
+
if (['zendesk', 'zendesk-support'].includes(normalized))
|
|
162
|
+
return 'zendesk';
|
|
163
|
+
if (['apple-search-ads', 'apple-ads', 'asa', 'search-ads'].includes(normalized))
|
|
164
|
+
return 'apple-search-ads';
|
|
165
|
+
if (['google-ads', 'adwords'].includes(normalized))
|
|
166
|
+
return 'google-ads';
|
|
167
|
+
if (['meta-ads', 'facebook-ads', 'instagram-ads', 'fb-ads'].includes(normalized))
|
|
168
|
+
return 'meta-ads';
|
|
169
|
+
if (['tiktok-ads', 'tiktok-business', 'tiktok-business-api'].includes(normalized))
|
|
170
|
+
return 'tiktok-ads';
|
|
171
|
+
if (['vercel', 'vercel-deployments', 'vercel-hosting'].includes(normalized))
|
|
172
|
+
return 'vercel';
|
|
173
|
+
if (['cloudflare', 'cf', 'cloudflare-workers', 'cloudflare-pages'].includes(normalized))
|
|
174
|
+
return 'cloudflare';
|
|
175
|
+
if (['resend', 'resend-email'].includes(normalized))
|
|
176
|
+
return 'resend';
|
|
177
|
+
if (['customerio', 'customer-io', 'customer.io', 'cio'].includes(normalized))
|
|
178
|
+
return 'customerio';
|
|
179
|
+
if (['mailchimp', 'mailchimp-marketing'].includes(normalized))
|
|
180
|
+
return 'mailchimp';
|
|
181
|
+
if (['appfollow', 'app-follow'].includes(normalized))
|
|
182
|
+
return 'appfollow';
|
|
183
|
+
if (['apptweak', 'app-tweak'].includes(normalized))
|
|
184
|
+
return 'apptweak';
|
|
185
|
+
if (['linear', 'linear-issues', 'linear-planning'].includes(normalized))
|
|
186
|
+
return 'linear';
|
|
187
|
+
if (['postiz', 'postiz-api', 'social-publishing', 'social-scheduler'].includes(normalized))
|
|
188
|
+
return 'postiz';
|
|
104
189
|
return null;
|
|
105
190
|
}
|
|
106
191
|
function parseConnectorList(value) {
|
|
@@ -110,14 +195,10 @@ function parseConnectorList(value) {
|
|
|
110
195
|
for (const entry of String(value).split(',')) {
|
|
111
196
|
const connector = normalizeConnectorKey(entry);
|
|
112
197
|
if (!connector) {
|
|
113
|
-
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use
|
|
198
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
|
|
114
199
|
}
|
|
115
200
|
if (connector === 'all') {
|
|
116
|
-
connectors.add(
|
|
117
|
-
connectors.add('github');
|
|
118
|
-
connectors.add('asc');
|
|
119
|
-
connectors.add('revenuecat');
|
|
120
|
-
connectors.add('sentry');
|
|
201
|
+
SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
|
|
121
202
|
}
|
|
122
203
|
else {
|
|
123
204
|
connectors.add(connector);
|
|
@@ -150,19 +231,28 @@ function replaceLegacyRuntimeScriptCommand(command) {
|
|
|
150
231
|
const trimmed = String(command || '').trim();
|
|
151
232
|
if (!trimmed)
|
|
152
233
|
return trimmed;
|
|
153
|
-
return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
234
|
+
return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-paddle-summary\.mjs|export-seo-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
154
235
|
}
|
|
155
236
|
function commandHasConfigArg(command) {
|
|
156
237
|
return /(?:^|\s)--config(?:=|\s|$)/.test(String(command || ''));
|
|
157
238
|
}
|
|
158
|
-
function
|
|
159
|
-
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-sentry-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
239
|
+
function commandIsBuiltinExporter(command) {
|
|
240
|
+
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-paddle-summary|export-seo-summary|export-sentry-summary|export-coolify-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
241
|
+
}
|
|
242
|
+
function commandSupportsActiveConfig(command) {
|
|
243
|
+
return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-sentry-summary|export-coolify-summary)\.mjs(?:\s|$)/.test(String(command || ''));
|
|
160
244
|
}
|
|
161
245
|
function withActiveConfigArg(command, configPath) {
|
|
162
246
|
const trimmed = String(command || '').trim();
|
|
163
|
-
if (!trimmed || !configPath || !
|
|
247
|
+
if (!trimmed || !configPath || !commandIsBuiltinExporter(trimmed)) {
|
|
164
248
|
return trimmed;
|
|
165
249
|
}
|
|
250
|
+
if (!commandSupportsActiveConfig(trimmed)) {
|
|
251
|
+
return trimmed
|
|
252
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, '$1')
|
|
253
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, '$1')
|
|
254
|
+
.trim();
|
|
255
|
+
}
|
|
166
256
|
if (commandHasConfigArg(trimmed)) {
|
|
167
257
|
return trimmed
|
|
168
258
|
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${shellQuote(configPath)}`)
|
|
@@ -187,12 +277,21 @@ function resolveShellCommand() {
|
|
|
187
277
|
}
|
|
188
278
|
return 'sh';
|
|
189
279
|
}
|
|
280
|
+
function hardenUnattendedShellCommand(command) {
|
|
281
|
+
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
282
|
+
}
|
|
190
283
|
function runShell(command, options = {}) {
|
|
191
284
|
return new Promise((resolve) => {
|
|
192
|
-
const child = spawn(resolveShellCommand(), ['-c', command], {
|
|
285
|
+
const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
|
|
193
286
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
287
|
cwd: options.cwd,
|
|
195
|
-
env:
|
|
288
|
+
env: {
|
|
289
|
+
...process.env,
|
|
290
|
+
...(options.env || {}),
|
|
291
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
292
|
+
SUDO_ASKPASS: '/bin/false',
|
|
293
|
+
SUDO_PROMPT: '',
|
|
294
|
+
},
|
|
196
295
|
});
|
|
197
296
|
let stdout = '';
|
|
198
297
|
let stderr = '';
|
|
@@ -604,9 +703,47 @@ async function testRevenueCatConnection(revenuecatToken, timeoutMs) {
|
|
|
604
703
|
};
|
|
605
704
|
}
|
|
606
705
|
}
|
|
706
|
+
async function testPaddleConnection(paddleToken, timeoutMs) {
|
|
707
|
+
if (!paddleToken) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
detail: 'missing token',
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const to = new Date().toISOString().slice(0, 10);
|
|
714
|
+
const fromDate = new Date();
|
|
715
|
+
fromDate.setUTCDate(fromDate.getUTCDate() - 2);
|
|
716
|
+
const from = fromDate.toISOString().slice(0, 10);
|
|
717
|
+
try {
|
|
718
|
+
const response = await fetchWithTimeout(`https://api.paddle.com/metrics/revenue?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, {
|
|
719
|
+
method: 'GET',
|
|
720
|
+
headers: {
|
|
721
|
+
Accept: 'application/json',
|
|
722
|
+
Authorization: `Bearer ${paddleToken}`,
|
|
723
|
+
'Paddle-Version': '1',
|
|
724
|
+
},
|
|
725
|
+
}, timeoutMs);
|
|
726
|
+
if (!response.ok) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
detail: `HTTP ${response.status}: ${truncate(response.body)}`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
ok: true,
|
|
734
|
+
detail: `HTTP ${response.status}`,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
607
744
|
function describeAnalyticsConnectionFailure(detail, analyticsTokenEnv, hasAnalyticsToken) {
|
|
608
745
|
if (!hasAnalyticsToken) {
|
|
609
|
-
return `AnalyticsCLI needs query access. Run \`
|
|
746
|
+
return `AnalyticsCLI needs query access. Run \`npx -y @analyticscli/growth-engineer@preview wizard --connectors analytics\`, create or copy a readonly CLI token in dash.analyticscli.com -> API Keys, and paste it into the local terminal wizard. Raw error: ${detail}`;
|
|
610
747
|
}
|
|
611
748
|
return `AnalyticsCLI connection failed with \`${analyticsTokenEnv}\` set. Verify that the pasted readonly CLI token is current and has project access. Raw error: ${detail}`;
|
|
612
749
|
}
|
|
@@ -643,6 +780,62 @@ async function testSentryConnection(sentryToken, timeoutMs, baseUrl = 'https://s
|
|
|
643
780
|
};
|
|
644
781
|
}
|
|
645
782
|
}
|
|
783
|
+
function normalizeCoolifyBaseUrl(value) {
|
|
784
|
+
const raw = String(value || '').trim().replace(/\/+$/, '');
|
|
785
|
+
if (!raw)
|
|
786
|
+
return '';
|
|
787
|
+
return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
788
|
+
}
|
|
789
|
+
function resolveCoolifyApiBaseUrl(baseUrl) {
|
|
790
|
+
const normalized = normalizeCoolifyBaseUrl(baseUrl);
|
|
791
|
+
if (!normalized)
|
|
792
|
+
return '';
|
|
793
|
+
if (/\/api\/v1$/i.test(normalized))
|
|
794
|
+
return normalized;
|
|
795
|
+
if (/\/api$/i.test(normalized))
|
|
796
|
+
return `${normalized}/v1`;
|
|
797
|
+
return `${normalized}/api/v1`;
|
|
798
|
+
}
|
|
799
|
+
async function testCoolifyConnection(coolifyToken, timeoutMs, baseUrl) {
|
|
800
|
+
if (!coolifyToken) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
detail: 'missing token',
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const apiBaseUrl = resolveCoolifyApiBaseUrl(baseUrl);
|
|
807
|
+
if (!apiBaseUrl) {
|
|
808
|
+
return {
|
|
809
|
+
ok: false,
|
|
810
|
+
detail: 'missing base URL',
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
const response = await fetchWithTimeout(`${apiBaseUrl}/applications?limit=1`, {
|
|
815
|
+
method: 'GET',
|
|
816
|
+
headers: {
|
|
817
|
+
Accept: 'application/json',
|
|
818
|
+
Authorization: `Bearer ${coolifyToken}`,
|
|
819
|
+
},
|
|
820
|
+
}, timeoutMs);
|
|
821
|
+
if (!response.ok) {
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
detail: `HTTP ${response.status}: ${truncate(response.body)}`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
ok: true,
|
|
829
|
+
detail: `HTTP ${response.status}`,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
return {
|
|
834
|
+
ok: false,
|
|
835
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
646
839
|
function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
647
840
|
const sentrySource = config?.sources?.sentry;
|
|
648
841
|
const accounts = Array.isArray(sentrySource?.accounts) ? sentrySource.accounts : [];
|
|
@@ -654,6 +847,13 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
|
654
847
|
label: String(account?.label || account?.name || account?.id || `Sentry ${index + 1}`).trim(),
|
|
655
848
|
tokenEnv: String(account?.tokenEnv || account?.token_env || account?.secretEnv || sentryTokenEnv).trim(),
|
|
656
849
|
baseUrl: String(account?.baseUrl || account?.base_url || account?.url || 'https://sentry.io').trim(),
|
|
850
|
+
org: String(account?.org || account?.organization || '').trim(),
|
|
851
|
+
projects: Array.isArray(account?.projects)
|
|
852
|
+
? account.projects.map((project) => String(typeof project === 'string' ? project : project?.project || project?.slug || '').trim()).filter(Boolean)
|
|
853
|
+
: account?.project
|
|
854
|
+
? [String(account.project).trim()].filter(Boolean)
|
|
855
|
+
: [],
|
|
856
|
+
environment: String(account?.environment || process.env.SENTRY_ENVIRONMENT || 'production').trim(),
|
|
657
857
|
}));
|
|
658
858
|
}
|
|
659
859
|
return [
|
|
@@ -662,9 +862,24 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
|
662
862
|
label: 'Sentry',
|
|
663
863
|
tokenEnv: sentryTokenEnv,
|
|
664
864
|
baseUrl: String(process.env.SENTRY_BASE_URL || 'https://sentry.io').trim(),
|
|
865
|
+
org: String(process.env.SENTRY_ORG || '').trim(),
|
|
866
|
+
projects: String(process.env.SENTRY_PROJECT || '').trim() ? [String(process.env.SENTRY_PROJECT).trim()] : [],
|
|
867
|
+
environment: String(process.env.SENTRY_ENVIRONMENT || 'production').trim(),
|
|
665
868
|
},
|
|
666
869
|
];
|
|
667
870
|
}
|
|
871
|
+
function describeSentryAccountTarget(account) {
|
|
872
|
+
const parts = [
|
|
873
|
+
account.label,
|
|
874
|
+
`id=${account.key}`,
|
|
875
|
+
`baseUrl=${account.baseUrl || 'https://sentry.io'}`,
|
|
876
|
+
account.org ? `org=${account.org}` : null,
|
|
877
|
+
account.projects?.length ? `projects=${account.projects.join(',')}` : null,
|
|
878
|
+
account.environment ? `environment=${account.environment}` : null,
|
|
879
|
+
account.tokenEnv ? `tokenEnv=${account.tokenEnv}` : null,
|
|
880
|
+
].filter(Boolean);
|
|
881
|
+
return parts.join(' ');
|
|
882
|
+
}
|
|
668
883
|
async function testGitHubConnection(githubToken, githubRepo, timeoutMs, actionMode) {
|
|
669
884
|
if (!githubToken) {
|
|
670
885
|
return {
|
|
@@ -773,7 +988,10 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
|
|
|
773
988
|
const tasks = [];
|
|
774
989
|
const analyticsTokenEnv = getSecretName(config, 'analyticsTokenEnv', 'ANALYTICSCLI_ACCESS_TOKEN');
|
|
775
990
|
const revenuecatTokenEnv = getSecretName(config, 'revenuecatTokenEnv', 'REVENUECAT_API_KEY');
|
|
991
|
+
const paddleTokenEnv = getSecretName(config, 'paddleTokenEnv', 'PADDLE_API_KEY');
|
|
992
|
+
const gscTokenEnv = getSecretName(config, 'gscTokenEnv', 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN');
|
|
776
993
|
const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
|
|
994
|
+
const coolifyTokenEnv = getSecretName(config, 'coolifyTokenEnv', 'COOLIFY_API_TOKEN');
|
|
777
995
|
const feedbackTokenEnv = getSecretName(config, 'feedbackTokenEnv', 'FEEDBACK_API_TOKEN');
|
|
778
996
|
const githubTokenEnv = getSecretName(config, 'githubTokenEnv', 'GITHUB_TOKEN');
|
|
779
997
|
const githubRepo = isConfiguredGitHubRepo(config?.project?.githubRepo)
|
|
@@ -840,6 +1058,81 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
|
|
|
840
1058
|
},
|
|
841
1059
|
});
|
|
842
1060
|
}
|
|
1061
|
+
const paddleSource = config.sources?.paddle;
|
|
1062
|
+
if (onlyAllows(onlyConnectors, 'paddle')) {
|
|
1063
|
+
scheduleProgressGroup(tasks, checks, progressJson, {
|
|
1064
|
+
key: 'paddle',
|
|
1065
|
+
label: 'Paddle',
|
|
1066
|
+
detail: 'metrics API auth + revenue read',
|
|
1067
|
+
run: async (groupChecks) => {
|
|
1068
|
+
if (sourceEnabled(config, 'paddle')) {
|
|
1069
|
+
const token = process.env[paddleTokenEnv] || '';
|
|
1070
|
+
if (!token) {
|
|
1071
|
+
addCheck(groupChecks, 'connection:paddle', false, `${paddleTokenEnv} missing (required for live Paddle metrics API test)`, paddleSource?.mode === 'command' ? 'fail' : 'warn');
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
const paddleConnection = await testPaddleConnection(token, timeoutMs);
|
|
1075
|
+
addCheck(groupChecks, 'connection:paddle', paddleConnection.ok, paddleConnection.ok
|
|
1076
|
+
? `Paddle metrics auth check passed (${paddleConnection.detail})`
|
|
1077
|
+
: `Paddle metrics auth check failed (${paddleConnection.detail})`);
|
|
1078
|
+
}
|
|
1079
|
+
if (paddleSource?.mode === 'command') {
|
|
1080
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(paddleSource.command || '').trim()), configPath);
|
|
1081
|
+
if (!command) {
|
|
1082
|
+
addCheck(groupChecks, 'connection:paddle-command', false, 'paddle source uses command mode but no command configured');
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const commandCheck = await testCommandSourceJson(`${command} --last 2d --max-signals 1`, commandCwd);
|
|
1086
|
+
addCheck(groupChecks, 'connection:paddle-command', commandCheck.ok, commandCheck.ok
|
|
1087
|
+
? 'Paddle command smoke test passed'
|
|
1088
|
+
: `Paddle command smoke test failed (${commandCheck.detail})`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
addCheck(groupChecks, 'connection:paddle', true, 'source disabled');
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const seoSource = config.sources?.seo;
|
|
1099
|
+
if (onlyAllows(onlyConnectors, 'seo')) {
|
|
1100
|
+
scheduleProgressGroup(tasks, checks, progressJson, {
|
|
1101
|
+
key: 'seo',
|
|
1102
|
+
label: 'SEO / GSC',
|
|
1103
|
+
detail: 'Search Console auth or CSV/DataForSEO config',
|
|
1104
|
+
run: async (groupChecks) => {
|
|
1105
|
+
if (sourceEnabled(config, 'seo')) {
|
|
1106
|
+
const hasGscCredential = Boolean(process.env[gscTokenEnv] ||
|
|
1107
|
+
process.env.GSC_ACCESS_TOKEN ||
|
|
1108
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
|
|
1109
|
+
process.env.GSC_SERVICE_ACCOUNT_JSON ||
|
|
1110
|
+
process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
|
|
1111
|
+
addCheck(groupChecks, 'connection:seo:gsc-credentials', hasGscCredential || seoSource?.mode !== 'command', hasGscCredential
|
|
1112
|
+
? 'GSC credential is configured'
|
|
1113
|
+
: 'GSC credential missing; command can still run in CSV-only mode if --gsc-csv/--csv is configured', hasGscCredential ? 'pass' : 'warn');
|
|
1114
|
+
addCheck(groupChecks, 'connection:seo:gsc-site', true, process.env.GSC_SITE_URL || seoSource?.siteUrl || seoSource?.site_url
|
|
1115
|
+
? 'GSC site/property is pinned intentionally'
|
|
1116
|
+
: 'no GSC site/property pinned; exporter will list and query all verified Search Console properties', 'pass');
|
|
1117
|
+
if (seoSource?.mode === 'command') {
|
|
1118
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(seoSource.command || '').trim()), configPath);
|
|
1119
|
+
if (!command) {
|
|
1120
|
+
addCheck(groupChecks, 'connection:seo-command', false, 'seo source uses command mode but no command configured');
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
const commandCheck = await testCommandSourceJson(`${command} --row-limit 5 --max-signals 1`, commandCwd);
|
|
1124
|
+
addCheck(groupChecks, 'connection:seo-command', commandCheck.ok, commandCheck.ok
|
|
1125
|
+
? 'SEO command smoke test passed'
|
|
1126
|
+
: `SEO command smoke test failed (${commandCheck.detail})`, hasGscCredential || /--csv|--gsc-csv/.test(command) ? 'fail' : 'warn');
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
addCheck(groupChecks, 'connection:seo', true, 'source disabled');
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
843
1136
|
const sentrySource = config.sources?.sentry;
|
|
844
1137
|
if (onlyAllows(onlyConnectors, 'sentry')) {
|
|
845
1138
|
scheduleProgressGroup(tasks, checks, progressJson, {
|
|
@@ -852,14 +1145,15 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
|
|
|
852
1145
|
for (const account of sentryAccounts) {
|
|
853
1146
|
const token = process.env[account.tokenEnv] || '';
|
|
854
1147
|
const checkName = sentryAccounts.length > 1 ? `connection:sentry:${account.key}` : 'connection:sentry';
|
|
1148
|
+
const accountTarget = describeSentryAccountTarget(account);
|
|
855
1149
|
if (!token) {
|
|
856
|
-
addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${
|
|
1150
|
+
addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${accountTarget})`, sentrySource?.mode === 'command' ? 'fail' : 'warn');
|
|
857
1151
|
continue;
|
|
858
1152
|
}
|
|
859
1153
|
const sentryConnection = await testSentryConnection(token, timeoutMs, account.baseUrl);
|
|
860
1154
|
addCheck(groupChecks, checkName, sentryConnection.ok, sentryConnection.ok
|
|
861
|
-
? `${
|
|
862
|
-
: `${
|
|
1155
|
+
? `${accountTarget} auth check passed (${sentryConnection.detail})`
|
|
1156
|
+
: `${accountTarget} auth check failed (${sentryConnection.detail})`);
|
|
863
1157
|
}
|
|
864
1158
|
if (sentrySource?.mode === 'command') {
|
|
865
1159
|
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(sentrySource.command || '').trim()), configPath);
|
|
@@ -870,12 +1164,56 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
|
|
|
870
1164
|
const commandCheck = await testCommandSourceJson(`${command} --limit 1 --max-signals 1 --last 24h`, commandCwd);
|
|
871
1165
|
addCheck(groupChecks, 'connection:sentry-command', commandCheck.ok, commandCheck.ok
|
|
872
1166
|
? 'Sentry command smoke test passed'
|
|
873
|
-
: `Sentry command smoke test failed (${commandCheck.detail})`);
|
|
1167
|
+
: `Sentry command smoke test failed (${commandCheck.detail}); configured accounts: ${sentryAccounts.map(describeSentryAccountTarget).join(' | ')}`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
const requiredByConnectorSetup = onlyAllows(onlyConnectors, 'sentry') && onlyConnectors.length > 0;
|
|
1173
|
+
addCheck(groupChecks, 'connection:sentry', !requiredByConnectorSetup, requiredByConnectorSetup
|
|
1174
|
+
? 'selected Sentry connector is still disabled in sources.sentry'
|
|
1175
|
+
: 'source disabled');
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const coolifySource = config.sources?.coolify;
|
|
1181
|
+
if (onlyAllows(onlyConnectors, 'coolify')) {
|
|
1182
|
+
scheduleProgressGroup(tasks, checks, progressJson, {
|
|
1183
|
+
key: 'coolify',
|
|
1184
|
+
label: 'Coolify',
|
|
1185
|
+
detail: 'API key auth + deployment/resource read',
|
|
1186
|
+
run: async (groupChecks) => {
|
|
1187
|
+
if (sourceEnabled(config, 'coolify')) {
|
|
1188
|
+
const token = process.env[coolifySource?.tokenEnv || coolifySource?.secretEnv || coolifyTokenEnv] || '';
|
|
1189
|
+
const baseUrl = String(coolifySource?.baseUrl || coolifySource?.base_url || process.env.COOLIFY_BASE_URL || '').trim();
|
|
1190
|
+
if (!token) {
|
|
1191
|
+
addCheck(groupChecks, 'connection:coolify', false, `${coolifySource?.tokenEnv || coolifySource?.secretEnv || coolifyTokenEnv} missing (required for live Coolify API test)`, coolifySource?.mode === 'command' ? 'fail' : 'warn');
|
|
1192
|
+
}
|
|
1193
|
+
else if (!baseUrl) {
|
|
1194
|
+
addCheck(groupChecks, 'connection:coolify', false, 'COOLIFY_BASE_URL or sources.coolify.baseUrl missing (required for live Coolify API test)', coolifySource?.mode === 'command' ? 'fail' : 'warn');
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
const coolifyConnection = await testCoolifyConnection(token, timeoutMs, baseUrl);
|
|
1198
|
+
addCheck(groupChecks, 'connection:coolify', coolifyConnection.ok, coolifyConnection.ok
|
|
1199
|
+
? `Coolify auth check passed (${coolifyConnection.detail})`
|
|
1200
|
+
: `Coolify auth check failed (${coolifyConnection.detail})`);
|
|
1201
|
+
}
|
|
1202
|
+
if (coolifySource?.mode === 'command') {
|
|
1203
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(coolifySource.command || '').trim()), configPath);
|
|
1204
|
+
if (!command) {
|
|
1205
|
+
addCheck(groupChecks, 'connection:coolify-command', false, 'coolify source uses command mode but no command configured');
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
const commandCheck = await testCommandSourceJson(`${command} --limit 1 --max-signals 1 --last 24h`, commandCwd);
|
|
1209
|
+
addCheck(groupChecks, 'connection:coolify-command', commandCheck.ok, commandCheck.ok
|
|
1210
|
+
? 'Coolify command smoke test passed'
|
|
1211
|
+
: `Coolify command smoke test failed (${commandCheck.detail})`);
|
|
874
1212
|
}
|
|
875
1213
|
}
|
|
876
1214
|
}
|
|
877
1215
|
else {
|
|
878
|
-
addCheck(groupChecks, 'connection:
|
|
1216
|
+
addCheck(groupChecks, 'connection:coolify', true, 'source disabled');
|
|
879
1217
|
}
|
|
880
1218
|
},
|
|
881
1219
|
});
|
|
@@ -909,13 +1247,20 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
|
|
|
909
1247
|
}
|
|
910
1248
|
for (const extraSource of getAllSourceEntries(config).filter((source) => !source.builtIn)) {
|
|
911
1249
|
const serviceKind = classifyServiceKind(extraSource.service || extraSource.key);
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1250
|
+
const explicitConnectorKind = normalizeConnectorKey(extraSource.key || extraSource.service);
|
|
1251
|
+
const connectorKind = explicitConnectorKind && explicitConnectorKind !== 'all'
|
|
1252
|
+
? explicitConnectorKind
|
|
1253
|
+
: serviceKind === 'store'
|
|
1254
|
+
? 'asc'
|
|
1255
|
+
: serviceKind === 'revenue'
|
|
1256
|
+
? 'revenuecat'
|
|
1257
|
+
: serviceKind === 'crash'
|
|
1258
|
+
? 'sentry'
|
|
1259
|
+
: serviceKind === 'infrastructure'
|
|
1260
|
+
? 'coolify'
|
|
1261
|
+
: serviceKind === 'seo'
|
|
1262
|
+
? 'seo'
|
|
1263
|
+
: serviceKind;
|
|
919
1264
|
if (!onlyAllows(onlyConnectors, connectorKind))
|
|
920
1265
|
continue;
|
|
921
1266
|
const checkName = `connection:${extraSource.key}`;
|
|
@@ -1062,6 +1407,26 @@ async function main() {
|
|
|
1062
1407
|
const hasRevenuecatToken = Boolean(process.env[revenuecatTokenEnv]);
|
|
1063
1408
|
addCheck(checks, `secret:${revenuecatTokenEnv}`, hasRevenuecatToken, hasRevenuecatToken ? 'set (required for RevenueCat command mode)' : 'missing (required for RevenueCat command mode)');
|
|
1064
1409
|
}
|
|
1410
|
+
if (sourceName === 'paddle') {
|
|
1411
|
+
const paddleTokenEnv = getSecretName(config, 'paddleTokenEnv', 'PADDLE_API_KEY');
|
|
1412
|
+
const hasPaddleToken = Boolean(process.env[paddleTokenEnv]);
|
|
1413
|
+
addCheck(checks, `secret:${paddleTokenEnv}`, hasPaddleToken, hasPaddleToken ? 'set (required for Paddle command mode)' : 'missing (required for Paddle command mode)');
|
|
1414
|
+
}
|
|
1415
|
+
if (sourceName === 'seo') {
|
|
1416
|
+
const gscTokenEnv = getSecretName(config, 'gscTokenEnv', 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN');
|
|
1417
|
+
const hasSearchConsoleAuth = Boolean(process.env[gscTokenEnv] ||
|
|
1418
|
+
process.env.GSC_ACCESS_TOKEN ||
|
|
1419
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
|
|
1420
|
+
process.env.GSC_SERVICE_ACCOUNT_JSON ||
|
|
1421
|
+
process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
|
|
1422
|
+
const commandText = String(source.command || '');
|
|
1423
|
+
const csvOnly = /--csv|--gsc-csv/.test(commandText);
|
|
1424
|
+
addCheck(checks, `secret:${gscTokenEnv}`, hasSearchConsoleAuth || csvOnly, hasSearchConsoleAuth
|
|
1425
|
+
? 'set or service-account auth configured'
|
|
1426
|
+
: csvOnly
|
|
1427
|
+
? 'not required for configured CSV-only SEO command'
|
|
1428
|
+
: 'missing (required for GSC API mode; CSV-only mode may use --gsc-csv/--csv)', hasSearchConsoleAuth || csvOnly ? 'pass' : 'warn');
|
|
1429
|
+
}
|
|
1065
1430
|
if (sourceName === 'sentry') {
|
|
1066
1431
|
const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
|
|
1067
1432
|
for (const account of normalizeSentryAccounts(config, sentryTokenEnv)) {
|
|
@@ -1071,6 +1436,14 @@ async function main() {
|
|
|
1071
1436
|
: `missing (required for ${account.label} Sentry command mode)`);
|
|
1072
1437
|
}
|
|
1073
1438
|
}
|
|
1439
|
+
if (sourceName === 'coolify') {
|
|
1440
|
+
const coolifyTokenEnv = getSecretName(config, 'coolifyTokenEnv', 'COOLIFY_API_TOKEN');
|
|
1441
|
+
const tokenEnv = String(source.tokenEnv || source.secretEnv || coolifyTokenEnv).trim();
|
|
1442
|
+
const hasCoolifyToken = Boolean(process.env[tokenEnv]);
|
|
1443
|
+
const hasCoolifyBaseUrl = Boolean(source.baseUrl || source.base_url || process.env.COOLIFY_BASE_URL);
|
|
1444
|
+
addCheck(checks, `secret:${tokenEnv}`, hasCoolifyToken, hasCoolifyToken ? 'set (required for Coolify command mode)' : 'missing (required for Coolify command mode)');
|
|
1445
|
+
addCheck(checks, 'source:coolify:base-url', hasCoolifyBaseUrl, hasCoolifyBaseUrl ? 'configured' : 'missing COOLIFY_BASE_URL or sources.coolify.baseUrl');
|
|
1446
|
+
}
|
|
1074
1447
|
if (!source.builtIn && source.secretEnv) {
|
|
1075
1448
|
const hasConnectorToken = Boolean(process.env[source.secretEnv]);
|
|
1076
1449
|
addCheck(checks, `secret:${source.secretEnv}`, hasConnectorToken, hasConnectorToken
|