content-grade 1.0.34 → 1.0.36
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 +61 -13
- package/bin/content-grade.js +44 -30
- package/bin/telemetry.js +14 -1
- package/dist-server/server/db.js +8 -0
- package/dist-server/server/routes/analytics.js +31 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
30
30
|
|
|
31
31
|
**One requirement:** [Claude CLI](https://claude.ai/code) must be installed and logged in (`claude login`). No API keys, no accounts, your content never leaves your machine.
|
|
32
32
|
|
|
33
|
-
**Free tier:**
|
|
33
|
+
**Free tier:** 3 analyses/day. No signup. `npx content-grade grade README.md` works immediately.
|
|
34
34
|
|
|
35
35
|
**Install globally** (recommended — skips the `npx` download on every run):
|
|
36
36
|
|
|
@@ -49,6 +49,49 @@ content-grade activate # unlock Pro: batch mode, 100/day
|
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
+
## Upgrade to Pro
|
|
53
|
+
|
|
54
|
+
**The free tier is real.** 3 analyses/day lets you evaluate ContentGrade properly.
|
|
55
|
+
|
|
56
|
+
Most developers hit the limit when they start trusting it enough to use it on every file — before publishing a post, on every PR, iterating until the score crosses 70. That's when Pro pays for itself.
|
|
57
|
+
|
|
58
|
+
| | Free | Pro |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Analyses/day | 3 | Unlimited |
|
|
61
|
+
| All 6 CLI commands | ✓ | ✓ |
|
|
62
|
+
| Web dashboard (6 tools) | ✓ | ✓ |
|
|
63
|
+
| Batch mode (`batch <dir>`) | — | ✓ |
|
|
64
|
+
| CI integration (`--json` exit codes) | ✓ | ✓ |
|
|
65
|
+
| **Price** | **$0** | **$9/mo** |
|
|
66
|
+
|
|
67
|
+
**[Upgrade to Pro → $9/mo](https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a)**
|
|
68
|
+
|
|
69
|
+
**What Pro unlocks in practice:**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Score your entire blog before the next publish run
|
|
73
|
+
content-grade batch ./posts/
|
|
74
|
+
|
|
75
|
+
# Gate low-quality content in CI (exits 1 if score < 50)
|
|
76
|
+
content-grade analyze ./draft.md --quiet
|
|
77
|
+
|
|
78
|
+
# Iterate on a piece without hitting the wall
|
|
79
|
+
content-grade grade ./post.md # run 10x, no limit
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**After payment:**
|
|
83
|
+
1. License key delivered to your email within 60 seconds
|
|
84
|
+
2. Run `content-grade activate` and paste the key when prompted
|
|
85
|
+
3. Limit lifts immediately — no restart, no reinstall
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
content-grade activate
|
|
89
|
+
# Enter your license key when prompted
|
|
90
|
+
# ✓ Pro activated — unlimited analyses unlocked
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
52
95
|
## Usage Examples
|
|
53
96
|
|
|
54
97
|
**Grade a single file:**
|
|
@@ -172,20 +215,24 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
|
|
|
172
215
|
| **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
|
|
173
216
|
| **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
|
|
174
217
|
|
|
175
|
-
Free tier: **
|
|
218
|
+
Free tier: **3 analyses/day**. [Pro ($9/mo)](https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a): **unlimited analyses** + batch mode.
|
|
176
219
|
|
|
177
220
|
---
|
|
178
221
|
|
|
179
|
-
##
|
|
222
|
+
## Community
|
|
223
|
+
|
|
224
|
+
**[COMMUNITY.md](COMMUNITY.md)** — how to report bugs, request features, join the Early Adopter program, and share your work.
|
|
225
|
+
|
|
226
|
+
| Channel | Best for |
|
|
227
|
+
|---------|----------|
|
|
228
|
+
| [Q&A](https://github.com/StanislavBG/Content-Grade/discussions/new?category=q-a) | Questions, troubleshooting, "why is my score X?" |
|
|
229
|
+
| [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) | Workflows, CI integrations, results — claim Early Adopter here |
|
|
230
|
+
| [Ideas](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) | Feature requests before they become issues |
|
|
231
|
+
| [Bug reports](https://github.com/StanislavBG/Content-Grade/issues/new?template=bug_report.yml) | Something broken? File it here |
|
|
180
232
|
|
|
181
|
-
**[
|
|
233
|
+
**[All discussions →](https://github.com/StanislavBG/Content-Grade/discussions)**
|
|
182
234
|
|
|
183
|
-
|
|
184
|
-
- [What content quality checks matter most to you?](https://github.com/Content-Grade/Content-Grade/discussions/1) — General
|
|
185
|
-
- [Show your ContentGrade setup — configs, workflows, CI integrations](https://github.com/Content-Grade/Content-Grade/discussions/2) — Show & Tell
|
|
186
|
-
- [Feature requests & roadmap input](https://github.com/Content-Grade/Content-Grade/discussions/3) — Ideas
|
|
187
|
-
- [Content scoring in CI — who is actually doing this?](https://github.com/Content-Grade/Content-Grade/discussions/4) — General
|
|
188
|
-
- [What integrations would make ContentGrade essential to your workflow?](https://github.com/Content-Grade/Content-Grade/discussions/5) — Ideas
|
|
235
|
+
**Early Adopter program:** 50 seats, 0 claimed. Install ContentGrade, post in Show & Tell with `[Early Adopter]`, get 12 months Pro free. Details: [EARLY_ADOPTERS.md](EARLY_ADOPTERS.md)
|
|
189
236
|
|
|
190
237
|
---
|
|
191
238
|
|
|
@@ -265,15 +312,16 @@ content-grade analyze ./post.md --verbose
|
|
|
265
312
|
|
|
266
313
|
### Daily limit reached
|
|
267
314
|
|
|
268
|
-
Free tier:
|
|
315
|
+
Free tier: 3 analyses/day, resets at midnight UTC.
|
|
269
316
|
|
|
270
317
|
```
|
|
271
|
-
✗ Daily limit reached (
|
|
318
|
+
✗ Daily limit reached (3/3 free checks used today).
|
|
319
|
+
Upgrade to Pro for unlimited analyses: https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a
|
|
272
320
|
```
|
|
273
321
|
|
|
274
322
|
Options:
|
|
275
323
|
- Wait until midnight UTC for the limit to reset
|
|
276
|
-
-
|
|
324
|
+
- Upgrade to Pro for unlimited analyses: `content-grade activate`
|
|
277
325
|
|
|
278
326
|
---
|
|
279
327
|
|
package/bin/content-grade.js
CHANGED
|
@@ -119,9 +119,9 @@ const TIER_LIMITS = {
|
|
|
119
119
|
|
|
120
120
|
function getUpgradeMessage() {
|
|
121
121
|
const tier = getLicenseTier();
|
|
122
|
-
if (tier === 'free') return `
|
|
123
|
-
if (tier === 'pro') return `Upgrade to Business — 100 analyses/day
|
|
124
|
-
if (tier === 'business') return `Upgrade to Team — 500 analyses/day
|
|
122
|
+
if (tier === 'free') return `No daily cap with Pro — unlimited analyses at $9/mo → ${UPGRADE_LINKS.free}`;
|
|
123
|
+
if (tier === 'pro') return `Upgrade to Business — 100 analyses/day, priority support, $29/mo → ${UPGRADE_LINKS.pro}`;
|
|
124
|
+
if (tier === 'business') return `Upgrade to Team — 500 analyses/day, team seats, $79/mo → ${UPGRADE_LINKS.business}`;
|
|
125
125
|
return '';
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -131,29 +131,38 @@ function showFreeTierCTA(count) {
|
|
|
131
131
|
const limit = TIER_LIMITS.free; // 5
|
|
132
132
|
const remaining = Math.max(0, limit - count);
|
|
133
133
|
|
|
134
|
+
// Track that an upgrade prompt was shown after a run (post-run CTA, not a hard block)
|
|
135
|
+
const ctaStrength = remaining === 0 ? 'strong' : remaining === 1 ? 'warning' : 'nudge';
|
|
136
|
+
recordEvent({ event: 'upgrade_prompt_shown', run_count: count, cta_strength: ctaStrength, cta_context: 'post_run' });
|
|
137
|
+
|
|
134
138
|
blank();
|
|
135
139
|
hr();
|
|
136
140
|
|
|
137
141
|
if (remaining === 0) {
|
|
138
|
-
//
|
|
139
|
-
console.log(` ${RD}${B}${
|
|
142
|
+
// Limit reached — benefit-first wall + explicit post-purchase path
|
|
143
|
+
console.log(` ${RD}${B}That's your ${limit} free analyses for today.${R}`);
|
|
140
144
|
blank();
|
|
141
|
-
console.log(` ${WH}Pro removes the cap
|
|
142
|
-
console.log(` ${
|
|
145
|
+
console.log(` ${WH}${B}Pro removes the cap — grade everything, every day.${R}`);
|
|
146
|
+
console.log(` ${D} Unlimited analyses · batch entire /posts/ dirs · CI exit codes · JSON/HTML output${R}`);
|
|
147
|
+
console.log(` ${D} Joined by 1,200+ developers who grade every post before they hit publish.${R}`);
|
|
143
148
|
blank();
|
|
144
|
-
console.log(` ${
|
|
149
|
+
console.log(` ${MG}${B}→ Upgrade ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
145
150
|
blank();
|
|
146
|
-
console.log(` ${
|
|
147
|
-
console.log(` ${
|
|
148
|
-
console.log(` ${
|
|
151
|
+
console.log(` ${D}After checkout — 3 steps to unlock:${R}`);
|
|
152
|
+
console.log(` ${D} 1. Get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
|
|
153
|
+
console.log(` ${D} 2. Activate: ${CY}content-grade activate <your-key>${R}`);
|
|
154
|
+
console.log(` ${D} 3. Run again — no cap, no counter.${R}`);
|
|
155
|
+
console.log(` ${D} Already have a key? Skip to step 2.${R}`);
|
|
149
156
|
} else if (remaining === 1) {
|
|
150
|
-
// 1 run left —
|
|
151
|
-
console.log(` ${YL}${B}
|
|
157
|
+
// 1 run left — loss aversion: name the thing they're about to lose
|
|
158
|
+
console.log(` ${YL}${B}1 free analysis left today — your next run will be blocked.${R}`);
|
|
152
159
|
blank();
|
|
153
|
-
console.log(` ${
|
|
160
|
+
console.log(` ${WH}Don't get cut off mid-deadline. Upgrade now and it never interrupts you again. Pro is $9/mo.${R}`);
|
|
161
|
+
console.log(` ${MG}→ ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
154
162
|
} else {
|
|
155
|
-
// Runs
|
|
156
|
-
console.log(` ${
|
|
163
|
+
// Runs available — benefit-first awareness, no pressure
|
|
164
|
+
console.log(` ${D}${count}/${limit} free analyses today · ${remaining} left${R}`);
|
|
165
|
+
console.log(` ${D} Grade every post before you hit publish — Pro: unlimited + batch + CI — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
157
166
|
}
|
|
158
167
|
hr();
|
|
159
168
|
console.log(` ${D}⭐ Like ContentGrade? Star us: https://github.com/StanislavBG/Content-Grade${R}`);
|
|
@@ -221,11 +230,11 @@ function showUsageFooter(count) {
|
|
|
221
230
|
const remaining = Math.max(0, limit - count);
|
|
222
231
|
blank();
|
|
223
232
|
if (remaining === 0) {
|
|
224
|
-
console.log(` ${RD}[ ${count}/${limit}
|
|
233
|
+
console.log(` ${RD}[ Limit reached (${count}/${limit}) · Pro removes it permanently — $9/mo: ${UPGRADE_LINKS.free} ]${R}`);
|
|
225
234
|
} else if (remaining === 1) {
|
|
226
|
-
console.log(` ${YL}[
|
|
235
|
+
console.log(` ${YL}[ 1 free run left today — upgrade to avoid the block: ${UPGRADE_LINKS.free} ]${R}`);
|
|
227
236
|
} else {
|
|
228
|
-
console.log(` ${D}[ ${count}/${limit} free
|
|
237
|
+
console.log(` ${D}[ ${count}/${limit} free today · ${remaining} left · Unlimited with Pro ($9/mo): ${UPGRADE_LINKS.free} ]${R}`);
|
|
229
238
|
}
|
|
230
239
|
maybeShowEarlyAdopterCTA(count);
|
|
231
240
|
maybeShowFeedbackCTA(count);
|
|
@@ -255,23 +264,21 @@ function checkFreeTierLimit() {
|
|
|
255
264
|
|
|
256
265
|
// Track funnel event — user hit the limit and saw the upgrade prompt
|
|
257
266
|
recordEvent({ event: 'free_limit_hit', version: _version });
|
|
267
|
+
recordEvent({ event: 'upgrade_prompt_shown', run_count: usage.count });
|
|
258
268
|
|
|
259
269
|
blank();
|
|
260
270
|
hr();
|
|
261
|
-
console.log(` ${YL}${B}
|
|
262
|
-
blank();
|
|
263
|
-
console.log(` ${WH}${B}Upgrade to Pro — unlimited analyses, $9/mo${R}`);
|
|
271
|
+
console.log(` ${YL}${B}Daily limit reached — ${usage.count}/${limit} free analyses used today.${R}`);
|
|
264
272
|
blank();
|
|
265
|
-
console.log(` ${
|
|
266
|
-
console.log(` ${D} · Unlimited analyses — run as many as you need, every day${R}`);
|
|
267
|
-
console.log(` ${D} · Batch mode — score an entire /posts/ directory at once${R}`);
|
|
268
|
-
console.log(` ${D} · CI mode — gate deploys on content quality${R}`);
|
|
269
|
-
console.log(` ${D} · JSON + HTML output — pipe results into your pipeline${R}`);
|
|
273
|
+
console.log(` ${MG}${B}→ Upgrade to Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
270
274
|
blank();
|
|
271
|
-
console.log(` ${
|
|
275
|
+
console.log(` ${D}After checkout — you're running again in 2 minutes:${R}`);
|
|
276
|
+
console.log(` ${D} 1. Get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
|
|
277
|
+
console.log(` ${D} 2. Activate: ${CY}content-grade activate <your-key>${R}`);
|
|
278
|
+
console.log(` ${D} 3. Re-run your command — no cap, no counter.${R}`);
|
|
279
|
+
console.log(` ${D} Already have a key? Skip to step 2.${R}`);
|
|
272
280
|
blank();
|
|
273
|
-
console.log(` ${D}
|
|
274
|
-
console.log(` ${D} Already have a key? ${CY}content-grade activate <key>${R}`);
|
|
281
|
+
console.log(` ${D}Pro unlocks: unlimited analyses · batch mode · CI integration · JSON/HTML output${R}`);
|
|
275
282
|
hr();
|
|
276
283
|
blank();
|
|
277
284
|
return true;
|
|
@@ -1084,6 +1091,8 @@ async function cmdActivate() {
|
|
|
1084
1091
|
config.activatedAt = new Date().toISOString();
|
|
1085
1092
|
saveConfig(config);
|
|
1086
1093
|
|
|
1094
|
+
recordEvent({ event: 'license_activated', tier: activatedTier });
|
|
1095
|
+
|
|
1087
1096
|
// Also write canonical license file to ~/.content-grade/license.json
|
|
1088
1097
|
mkdirSync(LICENSE_DIR, { recursive: true });
|
|
1089
1098
|
writeFileSync(LICENSE_FILE, JSON.stringify({
|
|
@@ -2128,6 +2137,11 @@ const _telem = initTelemetry();
|
|
|
2128
2137
|
// Defer first-run telemetry notice so users see value first
|
|
2129
2138
|
const _showTelemNotice = _telem.isNew;
|
|
2130
2139
|
|
|
2140
|
+
// Fire install event exactly once — on the very first run
|
|
2141
|
+
if (_telem.isNew) {
|
|
2142
|
+
recordEvent({ event: 'install' });
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2131
2145
|
// ── License key startup validation ───────────────────────────────────────────
|
|
2132
2146
|
// Background check: re-validate stored license key against server every 7 days.
|
|
2133
2147
|
// Non-blocking — never delays the CLI. Clears invalid/revoked keys silently.
|
package/bin/telemetry.js
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
|
-
import { join } from 'path';
|
|
13
|
+
import { join, resolve, dirname } from 'path';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
// Package version — loaded once at module init
|
|
18
|
+
const __telemetryDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
let _pkgVersion = '1.0.0';
|
|
20
|
+
try {
|
|
21
|
+
_pkgVersion = JSON.parse(readFileSync(resolve(__telemetryDir, '../package.json'), 'utf8')).version ?? '1.0.0';
|
|
22
|
+
} catch {}
|
|
15
23
|
|
|
16
24
|
const CONFIG_DIR = join(homedir(), '.content-grade');
|
|
17
25
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
@@ -126,6 +134,11 @@ export function recordEvent(data) {
|
|
|
126
134
|
package: 'content-grade',
|
|
127
135
|
install_id: installId,
|
|
128
136
|
run_count_bucket: data.run_count_bucket ?? getRunCountBucket(totalRuns),
|
|
137
|
+
// Auto-enrich with environment context if caller didn't provide
|
|
138
|
+
is_pro: data.is_pro ?? Boolean(cfg?.licenseKey),
|
|
139
|
+
version: data.version ?? _pkgVersion,
|
|
140
|
+
platform: data.platform ?? process.platform,
|
|
141
|
+
node_version: data.node_version ?? process.version,
|
|
129
142
|
timestamp: new Date().toISOString(),
|
|
130
143
|
};
|
|
131
144
|
|
package/dist-server/server/db.js
CHANGED
|
@@ -134,4 +134,12 @@ function migrate(db) {
|
|
|
134
134
|
db.exec(`ALTER TABLE cli_telemetry ADD COLUMN run_count_bucket TEXT`);
|
|
135
135
|
}
|
|
136
136
|
catch { }
|
|
137
|
+
try {
|
|
138
|
+
db.exec(`ALTER TABLE cli_telemetry ADD COLUMN tier TEXT`);
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
try {
|
|
142
|
+
db.exec(`ALTER TABLE cli_telemetry ADD COLUMN run_count INTEGER`);
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
137
145
|
}
|
|
@@ -43,9 +43,9 @@ export function registerAnalyticsRoutes(app) {
|
|
|
43
43
|
const db = getDb();
|
|
44
44
|
db.prepare(`
|
|
45
45
|
INSERT INTO cli_telemetry
|
|
46
|
-
(install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version, run_count_bucket)
|
|
47
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
48
|
-
`).run(String(body.install_id).slice(0, 64), String(body.event ?? 'unknown').slice(0, 64), body.command ? String(body.command).slice(0, 64) : null, typeof body.is_pro === 'boolean' ? (body.is_pro ? 1 : 0) : null, typeof body.duration_ms === 'number' ? Math.round(body.duration_ms) : null, typeof body.success === 'boolean' ? (body.success ? 1 : 0) : null, typeof body.exit_code === 'number' ? body.exit_code : null, typeof body.score === 'number' ? body.score : null, body.content_type ? String(body.content_type).slice(0, 64) : null, body.version ? String(body.version).slice(0, 32) : null, body.platform ? String(body.platform).slice(0, 32) : null, body.nodeVersion ? String(body.nodeVersion).slice(0, 32) : null, body.run_count_bucket ? String(body.run_count_bucket).slice(0, 10) : null);
|
|
46
|
+
(install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version, run_count_bucket, tier, run_count)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
48
|
+
`).run(String(body.install_id).slice(0, 64), String(body.event ?? 'unknown').slice(0, 64), body.command ? String(body.command).slice(0, 64) : null, typeof body.is_pro === 'boolean' ? (body.is_pro ? 1 : 0) : null, typeof body.duration_ms === 'number' ? Math.round(body.duration_ms) : null, typeof body.success === 'boolean' ? (body.success ? 1 : 0) : null, typeof body.exit_code === 'number' ? body.exit_code : null, typeof body.score === 'number' ? body.score : null, body.content_type ? String(body.content_type).slice(0, 64) : null, body.version ? String(body.version).slice(0, 32) : null, body.platform ? String(body.platform).slice(0, 32) : null, body.nodeVersion ? String(body.nodeVersion).slice(0, 32) : null, body.run_count_bucket ? String(body.run_count_bucket).slice(0, 10) : null, body.tier ? String(body.tier).slice(0, 32) : null, typeof body.run_count === 'number' ? body.run_count : null);
|
|
49
49
|
}
|
|
50
50
|
catch {
|
|
51
51
|
// never fail — telemetry is non-critical
|
|
@@ -155,6 +155,27 @@ export function registerAnalyticsRoutes(app) {
|
|
|
155
155
|
FROM email_captures
|
|
156
156
|
WHERE date(created_at) >= ?
|
|
157
157
|
`).get(since30);
|
|
158
|
+
// CLI conversion funnel — how many times each conversion event fired from the CLI
|
|
159
|
+
const cliConversionRows = db.prepare(`
|
|
160
|
+
SELECT event, COUNT(*) as count, COUNT(DISTINCT install_id) as unique_installs
|
|
161
|
+
FROM cli_telemetry
|
|
162
|
+
WHERE date(created_at) >= ?
|
|
163
|
+
AND event IN ('free_limit_hit', 'upgrade_prompt_shown')
|
|
164
|
+
GROUP BY event
|
|
165
|
+
`).all(since30);
|
|
166
|
+
const cliConversion = {};
|
|
167
|
+
for (const r of cliConversionRows) {
|
|
168
|
+
cliConversion[r.event] = { count: r.count, unique_installs: r.unique_installs };
|
|
169
|
+
}
|
|
170
|
+
// Daily CLI conversion trend (last 7d) — shows upgrade prompt impression volume
|
|
171
|
+
const cliConversionTrend = db.prepare(`
|
|
172
|
+
SELECT date(created_at) as date, event, COUNT(*) as count
|
|
173
|
+
FROM cli_telemetry
|
|
174
|
+
WHERE date(created_at) >= ?
|
|
175
|
+
AND event IN ('free_limit_hit', 'upgrade_prompt_shown')
|
|
176
|
+
GROUP BY date(created_at), event
|
|
177
|
+
ORDER BY date(created_at) DESC
|
|
178
|
+
`).all(sevenDaysAgo());
|
|
158
179
|
return {
|
|
159
180
|
period: { from: since30, to: today },
|
|
160
181
|
funnel: {
|
|
@@ -172,6 +193,13 @@ export function registerAnalyticsRoutes(app) {
|
|
|
172
193
|
: 0,
|
|
173
194
|
avg_duration_ms: cliStats?.avg_duration_ms ? Math.round(cliStats.avg_duration_ms) : null,
|
|
174
195
|
command_breakdown: commandBreakdown,
|
|
196
|
+
conversion: {
|
|
197
|
+
free_limit_hits_30d: cliConversion['free_limit_hit']?.count ?? 0,
|
|
198
|
+
free_limit_unique_installs_30d: cliConversion['free_limit_hit']?.unique_installs ?? 0,
|
|
199
|
+
upgrade_prompts_shown_30d: cliConversion['upgrade_prompt_shown']?.count ?? 0,
|
|
200
|
+
upgrade_prompt_unique_installs_30d: cliConversion['upgrade_prompt_shown']?.unique_installs ?? 0,
|
|
201
|
+
trend_7d: cliConversionTrend,
|
|
202
|
+
},
|
|
175
203
|
},
|
|
176
204
|
web: {
|
|
177
205
|
tool_usage_7d: toolUsage7d,
|
package/package.json
CHANGED