cchubber 0.5.6 → 0.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cchubber",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "What you spent. Why you spent it. Is that normal. — Claude Code usage diagnosis with beautiful HTML reports.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,22 +8,34 @@
8
8
  * Uses z-score anomaly detection (2 SD) to flag days where value drops significantly.
9
9
  */
10
10
 
11
- export function analyzeValueTrend(dailyFromJSONL) {
11
+ export function analyzeValueTrend(dailyFromJSONL, calculatedDailyCosts, inflectionDate) {
12
12
  if (!dailyFromJSONL || dailyFromJSONL.length < 3) {
13
13
  return { available: false };
14
14
  }
15
15
 
16
+ // Build cost lookup from calculated costs (not raw costUSD which is 0 for subscription)
17
+ const costByDate = {};
18
+ if (calculatedDailyCosts) {
19
+ for (const d of calculatedDailyCosts) {
20
+ costByDate[d.date] = d.cost || 0;
21
+ }
22
+ }
23
+
16
24
  // Calculate per-day metrics
17
25
  const daily = dailyFromJSONL
18
26
  .filter(d => d.outputTokens > 0 && d.messageCount > 0)
19
- .map(d => ({
20
- date: d.date,
21
- outputPerMsg: Math.round(d.outputTokens / d.messageCount),
22
- outputPerDollar: d.cost > 0 ? Math.round(d.outputTokens / d.cost) : 0,
23
- outputTokens: d.outputTokens,
24
- messageCount: d.messageCount,
25
- cost: d.cost || 0,
26
- }));
27
+ .map(d => {
28
+ const cost = costByDate[d.date] || 0;
29
+ return {
30
+ date: d.date,
31
+ outputPerMsg: Math.round(d.outputTokens / d.messageCount),
32
+ outputPerDollar: cost > 0 ? Math.round(d.outputTokens / cost) : 0,
33
+ wordsPerMsg: Math.round(d.outputTokens / d.messageCount / 1.3), // ~1.3 tokens per word
34
+ outputTokens: d.outputTokens,
35
+ messageCount: d.messageCount,
36
+ cost,
37
+ };
38
+ });
27
39
 
28
40
  if (daily.length < 3) return { available: false };
29
41
 
@@ -36,10 +48,15 @@ export function analyzeValueTrend(dailyFromJSONL) {
36
48
  // Recent 7 days vs older — is value declining?
37
49
  const sorted = [...daily].sort((a, b) => a.date.localeCompare(b.date));
38
50
  const recent = sorted.slice(-7);
39
- const older = sorted.slice(0, -7);
51
+ // If inflection date exists, use pre-inflection as the "before" baseline
52
+ // This gives a clean comparison: healthy baseline vs current reality
53
+ const older = inflectionDate
54
+ ? sorted.filter(d => d.date < inflectionDate)
55
+ : sorted.slice(0, -7);
40
56
 
41
57
  let trend = 'stable';
42
58
  let trendDetail = '';
59
+ let comparison = null;
43
60
 
44
61
  if (older.length >= 3) {
45
62
  const recentAvgPerMsg = Math.round(recent.reduce((s, d) => s + d.outputPerMsg, 0) / recent.length);
@@ -64,6 +81,14 @@ export function analyzeValueTrend(dailyFromJSONL) {
64
81
  } else {
65
82
  trendDetail = `Output per message stable (${recentAvgPerMsg} tokens/msg, was ${olderAvgPerMsg})`;
66
83
  }
84
+
85
+ // Expose before/after for visual comparison
86
+ comparison = {
87
+ olderAvgPerMsg, recentAvgPerMsg, msgChangePct: Math.round(msgChange),
88
+ olderAvgPerDollar, recentAvgPerDollar, dollarChangePct: Math.round(dollarChange),
89
+ olderWords: Math.round(olderAvgPerMsg / 1.3),
90
+ recentWords: Math.round(recentAvgPerMsg / 1.3),
91
+ };
67
92
  }
68
93
 
69
94
  // Z-score anomaly detection — flag days where value drops >2 SD
@@ -89,14 +114,44 @@ export function analyzeValueTrend(dailyFromJSONL) {
89
114
  }
90
115
  }
91
116
 
117
+ // Cost per message anomalies — days where each message cost way more than usual (caching broke)
118
+ const costPerMsgValues = daily.filter(d => d.cost > 0).map(d => d.cost / d.messageCount);
119
+ const cpmMean = costPerMsgValues.length > 0 ? costPerMsgValues.reduce((s, v) => s + v, 0) / costPerMsgValues.length : 0;
120
+ const cpmVariance = costPerMsgValues.length > 0 ? costPerMsgValues.reduce((s, v) => s + Math.pow(v - cpmMean, 2), 0) / costPerMsgValues.length : 0;
121
+ const cpmStdDev = Math.sqrt(cpmVariance);
122
+
123
+ // Use median as baseline — more robust to outliers than mean/z-score
124
+ const sortedCpm = [...costPerMsgValues].sort((a, b) => a - b);
125
+ const cpmMedian = sortedCpm.length > 0 ? sortedCpm[Math.floor(sortedCpm.length / 2)] : 0;
126
+
127
+ const costAnomalies = [];
128
+ if (cpmMedian > 0) {
129
+ for (const d of daily.filter(dd => dd.cost > 0 && dd.messageCount >= 10)) {
130
+ const cpm = d.cost / d.messageCount;
131
+ const multiplier = cpm / cpmMedian;
132
+ if (multiplier >= 1.8) { // 80%+ above median cost per message
133
+ costAnomalies.push({
134
+ date: d.date,
135
+ costPerMsg: Math.round(cpm * 100) / 100,
136
+ medianCostPerMsg: Math.round(cpmMedian * 100) / 100,
137
+ multiplier: Math.round(multiplier * 10) / 10,
138
+ });
139
+ }
140
+ }
141
+ }
142
+
92
143
  return {
93
144
  available: true,
94
- daily: daily.map(d => ({ date: d.date, outputPerMsg: d.outputPerMsg, outputPerDollar: d.outputPerDollar })),
145
+ daily: daily.map(d => ({ date: d.date, outputPerMsg: d.outputPerMsg, outputPerDollar: d.outputPerDollar, wordsPerMsg: d.wordsPerMsg })),
146
+ avgWordsPerMsg: Math.round(daily.reduce((s, d) => s + d.wordsPerMsg, 0) / daily.length),
95
147
  avgOutputPerMsg,
96
148
  avgOutputPerDollar,
97
149
  trend,
98
150
  trendDetail,
151
+ comparison,
99
152
  anomalies,
153
+ costAnomalies,
154
+ avgCostPerMsg: Math.round(cpmMean * 100) / 100,
100
155
  dayCount: daily.length,
101
156
  };
102
157
  }
package/src/cli/index.js CHANGED
@@ -135,7 +135,7 @@ async function main() {
135
135
  const inflection = detectInflectionPoints(dailyFromJSONL);
136
136
  const sessionIntel = analyzeSessionIntelligence(sessionMeta, jsonlEntries);
137
137
  const modelRouting = analyzeModelRouting(costAnalysis, jsonlEntries);
138
- const valueTrend = analyzeValueTrend(dailyFromJSONL);
138
+ const valueTrend = analyzeValueTrend(dailyFromJSONL, costAnalysis.dailyCosts, inflection?.date);
139
139
  if (valueTrend.available) console.log(` ✓ Value trend: ${valueTrend.avgOutputPerMsg} tokens/msg avg (${valueTrend.trend})`);
140
140
 
141
141
  const recommendations = generateRecommendations(costAnalysis, cacheHealth, claudeMdStack, anomalies, inflection, sessionIntel, modelRouting, projectBreakdown);
@@ -1,3 +1,10 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
7
+
1
8
  function esc(s) {
2
9
  if (!s) return '';
3
10
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
@@ -18,6 +25,7 @@ export function renderHTML(report) {
18
25
 
19
26
  const dailyCostsJSON = JSON.stringify(dailyCosts.map(d => ({
20
27
  date: d.date, cost: d.cost, cacheOutputRatio: d.cacheOutputRatio || 0, isAnomaly: anomalyDates.has(d.date),
28
+ out: d.outputTokens || 0, inp: d.inputTokens || 0, cr: d.cacheReadTokens || 0,
21
29
  })));
22
30
 
23
31
  const projectsJSON = JSON.stringify((projectBreakdown || []).map(p => ({
@@ -208,6 +216,7 @@ export function renderHTML(report) {
208
216
  <header class="w-full px-6 py-5 max-w-[1200px] mx-auto flex justify-between items-baseline">
209
217
  <div class="flex items-baseline gap-4">
210
218
  <a href="https://github.com/azkhh/cchubber" target="_blank" class="text-lg font-bold tracking-tight text-[#e3e2e3]" style="text-decoration:none;">CC Hubber</a>
219
+ <span class="text-[10px] font-mono text-[#908fa0]">v${PKG_VERSION}</span>
211
220
  <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0]">shipped fast with <a href="https://moveros.dev" target="_blank" style="text-decoration:none;color:inherit;">Mover OS</a></span>
212
221
  </div>
213
222
  <span class="font-mono text-[11px] text-[#908fa0]" id="range-lbl">All time</span>
@@ -353,6 +362,7 @@ ${inflection && inflection.multiplier >= 1.5 ? `
353
362
  <span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${costAnalysis.sessions?.total || 0}</span>
354
363
  <span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${costAnalysis.sessions?.avgDurationMinutes ? Math.round(costAnalysis.sessions.avgDurationMinutes) + ' min avg' : ''}</span>
355
364
  </div>`}
365
+
356
366
  </section>
357
367
 
358
368
  <!-- 4. COST TREND CHART -->
@@ -372,38 +382,61 @@ ${inflection && inflection.multiplier >= 1.5 ? `
372
382
  <svg id="cost-chart-svg" viewBox="0 0 900 200" preserveAspectRatio="xMidYMid meet"></svg>
373
383
  </section>
374
384
 
375
- ${report.valueTrend?.available ? `
385
+ ${report.valueTrend?.available ? (() => {
386
+ const vt = report.valueTrend;
387
+ const comp = vt.comparison;
388
+ const trendColor = vt.trend === 'declining' ? '#ffb4ab' : vt.trend === 'improving' ? '#10b981' : '#908fa0';
389
+
390
+ // Build verdict around output per dollar — the metric people actually care about
391
+ let verdict, verdictColor, verdictDetail;
392
+ if (!comp || !comp.olderAvgPerDollar) {
393
+ verdict = 'Not enough data to compare yet. Run again next week.';
394
+ verdictColor = '#908fa0';
395
+ verdictDetail = '';
396
+ } else if (comp.dollarChangePct < -20) {
397
+ verdict = 'Yes. ' + Math.abs(comp.dollarChangePct) + '% less output per dollar recently.';
398
+ verdictColor = '#ffb4ab';
399
+ var limitImpact = Math.round(100 / (100 - Math.abs(comp.dollarChangePct)) * 100 - 100);
400
+ verdictDetail = 'Before: ' + comp.olderAvgPerDollar.toLocaleString() + ' tokens/$1. Now: ' + comp.recentAvgPerDollar.toLocaleString() + ' tokens/$1. In practice, your usage limits run out ~' + limitImpact + '% faster than before. This could be caused by cache changes, heavier Opus usage, or longer sessions.';
401
+ } else if (comp.dollarChangePct > 20) {
402
+ verdict = 'No. Actually getting ' + comp.dollarChangePct + '% more output per dollar recently.';
403
+ verdictColor = '#10b981';
404
+ verdictDetail = 'Before: ' + comp.olderAvgPerDollar.toLocaleString() + ' tokens/$1. Now: ' + comp.recentAvgPerDollar.toLocaleString() + ' tokens/$1.';
405
+ } else {
406
+ verdict = 'No. Your output per dollar is stable.';
407
+ verdictColor = '#10b981';
408
+ verdictDetail = comp.recentAvgPerDollar.toLocaleString() + ' output tokens per $1 (was ' + comp.olderAvgPerDollar.toLocaleString() + ').';
409
+ }
410
+
411
+ return `
376
412
  <!-- VALUE TREND -->
377
413
  <section class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
378
- <div class="flex items-center justify-between mb-6">
379
- <h3 class="text-xl font-bold text-[#e3e2e3]">Output Value</h3>
380
- <span class="text-[10px] font-mono text-[#908fa0]">${report.valueTrend.trend === 'declining' ? 'DECLINING' : report.valueTrend.trend === 'improving' ? 'IMPROVING' : 'STABLE'}</span>
381
- </div>
382
- <div class="grid grid-cols-2 gap-6 mb-6">
383
- <div class="bg-[#0d0e0f] p-5 rounded-lg">
384
- <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-2">Tokens per Message</span>
385
- <span class="font-mono text-2xl font-bold text-[#e3e2e3]">${report.valueTrend.avgOutputPerMsg.toLocaleString()}</span>
386
- <span class="text-[10px] text-[#908fa0] block mt-1">avg output tokens per turn</span>
387
- </div>
388
- <div class="bg-[#0d0e0f] p-5 rounded-lg">
389
- <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-2">Tokens per Dollar</span>
390
- <span class="font-mono text-2xl font-bold text-[#e3e2e3]">${report.valueTrend.avgOutputPerDollar.toLocaleString()}</span>
391
- <span class="text-[10px] text-[#908fa0] block mt-1">output tokens per $1 spent</span>
392
- </div>
393
- </div>
394
- ${report.valueTrend.trendDetail ? `<p class="text-[12px] text-[#908fa0] mb-4">${esc(report.valueTrend.trendDetail)}</p>` : ''}
395
- ${report.valueTrend.anomalies?.length > 0 ? `
396
- <div class="space-y-2">
397
- <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block">Low Output Days (2+ SD below average)</span>
398
- ${report.valueTrend.anomalies.slice(0, 5).map(a => `
414
+ <h3 class="text-xl font-bold text-[#e3e2e3] mb-4">Are you getting less for your money?</h3>
415
+ <p class="text-[15px] font-semibold mb-1" style="color:${verdictColor}">${verdict}</p>
416
+ ${verdictDetail ? '<p class="text-[12px] text-[#908fa0] mb-2">' + verdictDetail + '</p>' : ''}
417
+ ${inflection ? '<p class="text-[11px] text-[#908fa0] mb-4">A cache change was detected on ' + esc(inflection.date) + '. Your recent 7 days reflect where you are now.</p>' : ''}
418
+
419
+ ${(vt.anomalies?.length > 0 || vt.costAnomalies?.length > 0) ? `
420
+ <div class="mt-6 space-y-2">
421
+ ${vt.anomalies?.length > 0 ? `
422
+ <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Days with unusually short responses</span>
423
+ ${vt.anomalies.slice(0, 3).map(a => `
399
424
  <div class="p-3 bg-[#0d0e0f] rounded-lg flex items-center justify-between">
400
- <span class="text-[12px] font-mono text-[#908fa0]">${a.date}</span>
401
- <span class="text-[12px] text-[#e3e2e3]">${a.outputPerMsg} tokens/msg</span>
402
- <span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:rgba(255,180,171,0.15);color:#ffb4ab">${a.deviation}σ below</span>
403
- </div>`).join('')}
425
+ <span class="text-[11px] font-mono text-[#908fa0]">${a.date}</span>
426
+ <span class="text-[11px] text-[#e3e2e3]">~${Math.round(a.outputPerMsg / 1.3)} words/response</span>
427
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:rgba(255,180,171,0.15);color:#ffb4ab">${a.deviation}σ below avg</span>
428
+ </div>`).join('')}` : ''}
429
+ ${vt.costAnomalies?.length > 0 ? `
430
+ <span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mt-3 mb-1">Days where each message cost more than usual (${vt.costAnomalies.length} days)</span>
431
+ ${vt.costAnomalies.slice(0, 5).map(a => `
432
+ <div class="p-3 bg-[#0d0e0f] rounded-lg grid" style="grid-template-columns:100px 1fr auto;gap:12px;align-items:center">
433
+ <span class="text-[11px] font-mono text-[#908fa0]">${a.date}</span>
434
+ <span class="text-[11px] text-[#e3e2e3]">$${a.costPerMsg}/msg vs $${a.medianCostPerMsg} median</span>
435
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:rgba(255,180,171,0.15);color:#ffb4ab">${a.multiplier}x</span>
436
+ </div>`).join('')}` : ''}
404
437
  </div>` : ''}
405
- </section>
406
- ` : ''}
438
+ </section>`;
439
+ })() : ''}
407
440
 
408
441
  <!-- 5. SESSION INTELLIGENCE + MODEL DISTRIBUTION -->
409
442
  <section class="grid grid-cols-1 lg:grid-cols-2 gap-8">
@@ -554,9 +587,9 @@ ${report.valueTrend?.available ? `
554
587
  <th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">#</th>
555
588
  <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Grade</th>
556
589
  <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Ratio</th>
557
- <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Cost</th>
590
+ <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">Cost</th>
558
591
  <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Opus %</th>
559
- <th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Country</th>
592
+ <th class="px-4 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-left">Country</th>
560
593
  </tr>
561
594
  </thead>
562
595
  <tbody id="leaderboard-body"></tbody>
@@ -817,13 +850,14 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
817
850
  // hover targets
818
851
  d.forEach(function(x,j){
819
852
  var px=PD.l+(d.length===1?cW/2:j*step),py=PD.t+cH-(x.cost/mx)*cH;
820
- s+='<circle cx="'+px+'" cy="'+py+'" r="14" fill="transparent" data-d="'+x.date+'" data-c="'+x.cost+'" data-a="'+(x.isAnomaly?1:0)+'" class="hov" style="cursor:crosshair"/>';
853
+ s+='<circle cx="'+px+'" cy="'+py+'" r="14" fill="transparent" data-d="'+x.date+'" data-c="'+x.cost+'" data-a="'+(x.isAnomaly?1:0)+'" data-o="'+(x.out||0)+'" class="hov" style="cursor:crosshair"/>';
821
854
  });
822
855
  svg.innerHTML=s;
823
856
  svg.querySelectorAll('.hov').forEach(function(el){
824
857
  el.addEventListener('mouseenter',function(e){
825
858
  ttd.textContent=e.target.dataset.d;
826
- ttc.textContent=fc(parseFloat(e.target.dataset.c));
859
+ var ot=parseInt(e.target.dataset.o||0);
860
+ ttc.textContent=fc(parseFloat(e.target.dataset.c))+(ot>0?' — '+ft(ot)+' output':'');
827
861
  tta.textContent=e.target.dataset.a==='1'?'ANOMALY':'';
828
862
  tta.style.display=e.target.dataset.a==='1'?'block':'none';
829
863
  tt.classList.add('on');
@@ -838,7 +872,7 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
838
872
  function setR(r){
839
873
  var f=filt(r);chart(f);
840
874
  var ci=document.getElementById('chart-info');
841
- if(ci&&f.length){var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length;ci.textContent=a+' days \u00b7 '+fc(t)}
875
+ if(ci&&f.length){var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length,tok=f.reduce(function(s,x){return s+(x.out||0)},0);ci.textContent=a+' days \u00b7 '+fc(t)+' \u00b7 '+ft(tok)+' output'}
842
876
  var rl=document.getElementById('range-lbl');if(rl)rl.textContent=RL[r]||'All time';
843
877
  CARD.range=RL[r]||'All time';
844
878
  var cr=document.getElementById('card-range');if(cr)cr.textContent=CARD.range;
@@ -1197,9 +1231,9 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
1197
1231
  html += '<td class="px-8 py-3 text-sm font-mono '+(isMe?'text-[#c0c1ff] font-bold':'text-[#908fa0]')+'">#'+(idx+1)+(isMe?' ← you':'')+'</td>';
1198
1232
  html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[g]+'">'+g+'</td>';
1199
1233
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
1200
- html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost||'?')+'</td>';
1234
+ html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost?'$'+entry.cost:'?')+'</td>';
1201
1235
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus!=null?entry.opus:'?')+'%</td>';
1202
- html += '<td class="px-8 py-3 text-sm text-[#908fa0]">'+(isMe?'You':(entry.country||'?'))+'</td>';
1236
+ html += '<td class="px-4 py-3 text-sm text-[#908fa0]">'+(isMe?'You':(entry.country||'?'))+'</td>';
1203
1237
  html += '</tr>';
1204
1238
  }
1205
1239
  tbody.innerHTML = html;
package/src/telemetry.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import https from 'https';
2
2
  import { platform, arch, homedir, cpus, totalmem, freemem } from 'os';
3
3
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
4
- import { join } from 'path';
4
+ import { join, dirname } from 'path';
5
5
  import { execSync as rawExec } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
6
10
 
7
11
  // Suppress stderr output on Windows (prevents "system cannot find path" spam)
8
12
  function execSync(cmd, opts = {}) {
@@ -38,7 +42,7 @@ function markTelemetrySent() {
38
42
 
39
43
  export function sendTelemetry(report) {
40
44
  const payload = {
41
- v: '0.3.3',
45
+ v: PKG_VERSION,
42
46
  uid: getOrCreateUID(),
43
47
  ts: new Date().toISOString(),
44
48
  os: platform(),