@analyticscli/growth-engineer 0.1.1-preview.15 → 0.1.1-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.
@@ -28,6 +28,16 @@ export declare function buildAscSummary(input: any): {
28
28
  batchReports: any;
29
29
  productionVersions: any[];
30
30
  analytics: {
31
+ impressions: {
32
+ total: number;
33
+ previousTotal: number;
34
+ percentChange: number;
35
+ };
36
+ pageViewUnique: {
37
+ total: number;
38
+ previousTotal: number;
39
+ percentChange: number;
40
+ };
31
41
  units: {
32
42
  total: number;
33
43
  previousTotal: number;
@@ -49,9 +59,59 @@ export declare function buildAscSummary(input: any): {
49
59
  percentChange: number;
50
60
  nonZeroDays: any;
51
61
  };
62
+ crashes: {
63
+ total: number;
64
+ previousTotal: number;
65
+ percentChange: number;
66
+ };
67
+ sessions: {
68
+ total: number;
69
+ previousTotal: number;
70
+ percentChange: number;
71
+ };
72
+ activeDevices: {
73
+ total: number;
74
+ previousTotal: number;
75
+ percentChange: number;
76
+ };
77
+ installations: {
78
+ total: number;
79
+ previousTotal: number;
80
+ percentChange: number;
81
+ };
82
+ deletions: {
83
+ total: number;
84
+ previousTotal: number;
85
+ percentChange: number;
86
+ };
87
+ purchases: {
88
+ total: number;
89
+ previousTotal: number;
90
+ percentChange: number;
91
+ };
92
+ proceeds: {
93
+ total: number;
94
+ previousTotal: number;
95
+ percentChange: number;
96
+ };
97
+ payingUsers: {
98
+ total: number;
99
+ previousTotal: number;
100
+ percentChange: number;
101
+ };
102
+ subscriptions: {
103
+ total: number;
104
+ previousTotal: number;
105
+ percentChange: number;
106
+ };
107
+ trialStarts: {
108
+ total: number;
109
+ previousTotal: number;
110
+ percentChange: number;
111
+ };
52
112
  totalCrashes: any;
53
- crashBreakdown: any;
54
- sourceBreakdown: any;
113
+ crashBreakdown: any[];
114
+ sourceBreakdown: any[];
55
115
  overviewMetricCatalog: any[];
56
116
  };
57
117
  };
@@ -564,6 +564,30 @@ function summarizeSourceBreakdown(payload) {
564
564
  .filter((entry) => entry.pageViewUnique > 0)
565
565
  .sort((a, b) => b.pageViewUnique - a.pageViewUnique);
566
566
  }
567
+ function normalizeBatchSourceBreakdown(entries) {
568
+ return (Array.isArray(entries) ? entries : [])
569
+ .map((entry) => ({
570
+ key: String(entry?.key || entry?.title || 'unknown'),
571
+ title: String(entry?.title || entry?.key || 'Unknown'),
572
+ impressions: coerceNumber(entry?.impressions) || 0,
573
+ pageViewUnique: coerceNumber(entry?.pageViewUnique) || 0,
574
+ units: coerceNumber(entry?.units) || 0,
575
+ redownloads: coerceNumber(entry?.redownloads) || 0,
576
+ purchases: coerceNumber(entry?.purchases) || 0,
577
+ proceeds: coerceNumber(entry?.proceeds) || 0,
578
+ }))
579
+ .filter((entry) => entry.impressions > 0 ||
580
+ entry.pageViewUnique > 0 ||
581
+ entry.units > 0 ||
582
+ entry.redownloads > 0 ||
583
+ entry.purchases > 0 ||
584
+ entry.proceeds > 0)
585
+ .sort((a, b) => {
586
+ const aValue = a.pageViewUnique || a.impressions || a.units || a.purchases || a.proceeds || 0;
587
+ const bValue = b.pageViewUnique || b.impressions || b.units || b.purchases || b.proceeds || 0;
588
+ return bValue - aValue;
589
+ });
590
+ }
567
591
  function extractAscCrashBreakdowns(payload) {
568
592
  const breakdowns = Array.isArray(payload?.appUsageBreakdowns) ? payload.appUsageBreakdowns : [];
569
593
  return breakdowns
@@ -586,14 +610,6 @@ function totalAscCrashes(payload) {
586
610
  return directTotal;
587
611
  return extractAscCrashBreakdowns(payload).reduce((sum, entry) => sum + entry.value, 0);
588
612
  }
589
- function isLikelyAscWebAuthMissing(warnings) {
590
- return warnings.some((warning) => {
591
- const normalized = String(warning || '').toLowerCase();
592
- return (normalized.includes('asc web auth login') ||
593
- normalized.includes('web session is unauthorized') ||
594
- normalized.includes('web session is expired'));
595
- });
596
- }
597
613
  function collectAscOverviewMetricCatalog(payload) {
598
614
  const sections = ['acquisition', 'sales', 'subscriptions'];
599
615
  const metrics = [];
@@ -653,6 +669,38 @@ function collectAscOverviewMetricCatalog(payload) {
653
669
  }
654
670
  return [...byKey.values()];
655
671
  }
672
+ function mergeMetricCatalogs(...catalogs) {
673
+ const byKey = new Map();
674
+ for (const catalog of catalogs) {
675
+ for (const metric of Array.isArray(catalog) ? catalog : []) {
676
+ const section = String(metric?.section || 'unknown');
677
+ const measure = String(metric?.measure || '').trim();
678
+ if (!measure)
679
+ continue;
680
+ const key = `${section}:${measure}`;
681
+ if (!byKey.has(key)) {
682
+ byKey.set(key, {
683
+ section,
684
+ measure,
685
+ total: coerceNumber(metric?.total),
686
+ previousTotal: coerceNumber(metric?.previousTotal),
687
+ percentChange: coerceNumber(metric?.percentChange),
688
+ type: String(metric?.type || 'COUNT'),
689
+ });
690
+ }
691
+ }
692
+ }
693
+ return [...byKey.values()];
694
+ }
695
+ function metricSnapshot(metric) {
696
+ return metric
697
+ ? {
698
+ total: coerceNumber(metric.total) ?? 0,
699
+ previousTotal: coerceNumber(metric.previousTotal),
700
+ percentChange: coerceNumber(metric.percentChange),
701
+ }
702
+ : null;
703
+ }
656
704
  function formatMetricMovement(metric) {
657
705
  const total = metric.total === null ? 'unknown' : round(metric.total);
658
706
  const previous = metric.previousTotal === null ? null : ` previous ${round(metric.previousTotal)}`;
@@ -690,26 +738,47 @@ export function buildAscSummary(input) {
690
738
  ];
691
739
  const topThemes = rankKeywordThemes(reviewTexts).slice(0, 2);
692
740
  const analyticsMetricsPayload = input?.batchAnalyticsPayload || input?.analyticsMetricsPayload || input?.analyticsOverviewPayload;
741
+ const impressionsMetric = findAscMetric(analyticsMetricsPayload, 'impressions');
742
+ const pageViewsMetric = findAscMetric(analyticsMetricsPayload, 'pageViewUnique');
693
743
  const unitsMetric = findAscMetric(analyticsMetricsPayload, 'units');
694
744
  const redownloadsMetric = findAscMetric(analyticsMetricsPayload, 'redownloads');
695
745
  const conversionRateMetric = findAscMetric(analyticsMetricsPayload || input?.analyticsOverviewPayload, 'conversionRate');
696
746
  const crashRateMetric = findAscMetric(analyticsMetricsPayload, 'crashRate');
697
- const sourceBreakdown = summarizeSourceBreakdown(input?.analyticsSourcesPayload);
698
- const totalSourcePageViews = sourceBreakdown.reduce((sum, source) => sum + source.pageViewUnique, 0);
747
+ const crashesMetric = findAscMetric(analyticsMetricsPayload, 'crashes');
748
+ const sessionsMetric = findAscMetric(analyticsMetricsPayload, 'sessions');
749
+ const activeDevicesMetric = findAscMetric(analyticsMetricsPayload, 'activeDevices');
750
+ const installationsMetric = findAscMetric(analyticsMetricsPayload, 'installations');
751
+ const deletionsMetric = findAscMetric(analyticsMetricsPayload, 'deletions');
752
+ const purchasesMetric = findAscMetric(analyticsMetricsPayload, 'purchases');
753
+ const proceedsMetric = findAscMetric(analyticsMetricsPayload, 'proceeds');
754
+ const payingUsersMetric = findAscMetric(analyticsMetricsPayload, 'payingUsers');
755
+ const subscriptionsMetric = findAscMetric(analyticsMetricsPayload, 'subscriptions');
756
+ const trialStartsMetric = findAscMetric(analyticsMetricsPayload, 'trialStarts');
757
+ const sourceBreakdown = [
758
+ ...normalizeBatchSourceBreakdown(input?.batchAnalyticsPayload?.sourceBreakdown),
759
+ ...summarizeSourceBreakdown(input?.analyticsSourcesPayload),
760
+ ];
761
+ const totalSourcePageViews = sourceBreakdown.reduce((sum, source) => sum + (source.pageViewUnique || source.impressions || source.units || 0), 0);
699
762
  const topSource = sourceBreakdown[0] || null;
700
- const crashBreakdown = extractAscCrashBreakdowns(input?.analyticsOverviewPayload);
701
- const totalCrashes = totalAscCrashes(input?.analyticsOverviewPayload);
763
+ const crashBreakdown = [
764
+ ...(Array.isArray(input?.batchAnalyticsPayload?.crashBreakdown)
765
+ ? input.batchAnalyticsPayload.crashBreakdown
766
+ : []),
767
+ ...extractAscCrashBreakdowns(input?.analyticsOverviewPayload),
768
+ ]
769
+ .filter((entry) => coerceNumber(entry?.value) > 0)
770
+ .sort((a, b) => (coerceNumber(b?.value) || 0) - (coerceNumber(a?.value) || 0));
771
+ const totalCrashes = coerceNumber(crashesMetric?.total) ||
772
+ totalAscCrashes(input?.analyticsOverviewPayload) ||
773
+ crashBreakdown.reduce((sum, entry) => sum + (coerceNumber(entry?.value) || 0), 0);
702
774
  const analyticsWarnings = Array.isArray(input?.analyticsWarnings) ? input.analyticsWarnings : [];
703
- const webAuthMissing = isLikelyAscWebAuthMissing(analyticsWarnings);
704
775
  const batchReports = Array.isArray(input?.batchReports) ? input.batchReports : [];
705
- const analyticsAvailability = webAuthMissing
706
- ? 'web_auth_missing'
707
- : analyticsWarnings.some((warning) => String(warning).includes('403'))
708
- ? 'not_public_or_not_analytics_ready'
709
- : unitsMetric || conversionRateMetric || sourceBreakdown.length > 0 || totalCrashes > 0 || batchReports.length > 0
710
- ? 'available'
711
- : 'unknown';
712
- const overviewMetricCatalog = collectAscOverviewMetricCatalog(input?.analyticsOverviewPayload);
776
+ const analyticsAvailability = analyticsWarnings.some((warning) => String(warning).includes('403'))
777
+ ? 'not_public_or_not_analytics_ready'
778
+ : analyticsMetricsPayload || unitsMetric || conversionRateMetric || sourceBreakdown.length > 0 || totalCrashes > 0 || batchReports.length > 0
779
+ ? 'available'
780
+ : 'unknown';
781
+ const overviewMetricCatalog = mergeMetricCatalogs(collectAscOverviewMetricCatalog(input?.analyticsOverviewPayload), input?.batchAnalyticsPayload?.overviewMetricCatalog);
713
782
  const notableOverviewMetrics = overviewMetricCatalog
714
783
  .filter((metric) => {
715
784
  const measure = metric.measure.toLowerCase();
@@ -733,25 +802,6 @@ export function buildAscSummary(input) {
733
802
  .filter((point) => Number(point.value) > 0)
734
803
  .slice(-5);
735
804
  const signals = [];
736
- if (webAuthMissing) {
737
- maybePushSignal(signals, {
738
- id: 'asc_web_analytics_access_missing',
739
- title: 'ASC web analytics access needs login refresh',
740
- area: 'connector',
741
- priority: 'high',
742
- metric: 'asc_web_analytics_access',
743
- current_value: 0,
744
- baseline_value: 1,
745
- delta_percent: -100,
746
- evidence: analyticsWarnings.slice(0, 3),
747
- suggested_actions: [
748
- 'Ask the OpenClaw user whether to enable experimental ASC web analytics for the specific missing metric that API-key batch reports could not provide',
749
- 'If the user accepts, set ASC_WEB_APPLE_ID in the host terminal and run: asc web auth login --apple-id "$ASC_WEB_APPLE_ID"',
750
- 'Continue using API-key ASC batch reports if the user declines or the Apple Account web session expires again',
751
- ],
752
- keywords: ['asc', 'web_analytics', 'login', 'connector'],
753
- });
754
- }
755
805
  if (blockingStatuses.length > 0) {
756
806
  maybePushSignal(signals, {
757
807
  id: 'asc_release_blockers_detected',
@@ -873,6 +923,60 @@ export function buildAscSummary(input) {
873
923
  keywords: ['asc', 'analytics', 'overview_metrics', 'conversion', 'handlungsempfehlung'],
874
924
  });
875
925
  }
926
+ if (purchasesMetric || proceedsMetric || payingUsersMetric || subscriptionsMetric || trialStartsMetric) {
927
+ const purchases = coerceNumber(purchasesMetric?.total) || 0;
928
+ const proceeds = coerceNumber(proceedsMetric?.total) || 0;
929
+ const payingUsers = coerceNumber(payingUsersMetric?.total) || 0;
930
+ maybePushSignal(signals, {
931
+ id: 'asc_commerce_metrics_available',
932
+ title: 'ASC commerce metrics are available for store-to-revenue analysis',
933
+ area: 'revenue',
934
+ priority: 'low',
935
+ metric: purchasesMetric ? 'asc_purchases' : proceedsMetric ? 'asc_proceeds' : 'asc_commerce_metrics',
936
+ current_value: purchases || proceeds || payingUsers || coerceNumber(subscriptionsMetric?.total) || coerceNumber(trialStartsMetric?.total) || 0,
937
+ baseline_value: null,
938
+ delta_percent: null,
939
+ evidence: [
940
+ purchasesMetric ? `Purchases: ${purchases}` : null,
941
+ proceedsMetric ? `Developer proceeds: ${round(proceeds)}` : null,
942
+ payingUsersMetric ? `Paying users: ${payingUsers}` : null,
943
+ subscriptionsMetric ? `Subscriptions: ${coerceNumber(subscriptionsMetric.total) || 0}` : null,
944
+ trialStartsMetric ? `Trial starts: ${coerceNumber(trialStartsMetric.total) || 0}` : null,
945
+ ].filter(Boolean),
946
+ suggested_actions: [
947
+ 'Compare ASC commerce movement with RevenueCat/Paddle entitlement and subscription signals before changing pricing',
948
+ 'Use download source and purchase source dimensions to separate acquisition quality from paywall or product issues',
949
+ ],
950
+ keywords: ['asc', 'commerce', 'revenue', 'purchase', 'subscription'],
951
+ });
952
+ }
953
+ if (sessionsMetric || activeDevicesMetric || installationsMetric || deletionsMetric) {
954
+ maybePushSignal(signals, {
955
+ id: 'asc_usage_metrics_available',
956
+ title: 'ASC app usage metrics are available for retention checks',
957
+ area: 'retention',
958
+ priority: 'low',
959
+ metric: sessionsMetric ? 'asc_sessions' : activeDevicesMetric ? 'asc_active_devices' : 'asc_usage_metrics',
960
+ current_value: coerceNumber(sessionsMetric?.total) ||
961
+ coerceNumber(activeDevicesMetric?.total) ||
962
+ coerceNumber(installationsMetric?.total) ||
963
+ coerceNumber(deletionsMetric?.total) ||
964
+ 0,
965
+ baseline_value: null,
966
+ delta_percent: null,
967
+ evidence: [
968
+ sessionsMetric ? `Sessions: ${coerceNumber(sessionsMetric.total) || 0}` : null,
969
+ activeDevicesMetric ? `Active devices: ${coerceNumber(activeDevicesMetric.total) || 0}` : null,
970
+ installationsMetric ? `Installations: ${coerceNumber(installationsMetric.total) || 0}` : null,
971
+ deletionsMetric ? `Deletions: ${coerceNumber(deletionsMetric.total) || 0}` : null,
972
+ ].filter(Boolean),
973
+ suggested_actions: [
974
+ 'Compare ASC usage and deletion movement with AnalyticsCLI retention cohorts and first-session funnels',
975
+ 'Treat low-volume usage reports as directional because Apple applies privacy thresholds and opt-in limits',
976
+ ],
977
+ keywords: ['asc', 'usage', 'sessions', 'retention', 'deletions'],
978
+ });
979
+ }
876
980
  if (unitsMetric && coerceNumber(unitsMetric.total) !== null) {
877
981
  const units = coerceNumber(unitsMetric.total) || 0;
878
982
  const percentChange = coerceNumber(unitsMetric.percentChange);
@@ -928,24 +1032,25 @@ export function buildAscSummary(input) {
928
1032
  }
929
1033
  }
930
1034
  if (topSource && totalSourcePageViews > 0) {
931
- const share = topSource.pageViewUnique / totalSourcePageViews;
1035
+ const topSourceValue = topSource.pageViewUnique || topSource.impressions || topSource.units || topSource.purchases || topSource.proceeds || 0;
1036
+ const share = topSourceValue / totalSourcePageViews;
932
1037
  if (share >= 0.5 || sourceBreakdown.length >= 2) {
933
1038
  maybePushSignal(signals, {
934
1039
  id: 'asc_source_mix_available',
935
1040
  title: 'ASC source traffic is available for acquisition recommendations',
936
1041
  area: 'acquisition',
937
1042
  priority: share >= 0.7 ? 'medium' : 'low',
938
- metric: 'asc_source_page_view_unique',
939
- current_value: topSource.pageViewUnique,
1043
+ metric: topSource.pageViewUnique ? 'asc_source_page_view_unique' : topSource.impressions ? 'asc_source_impressions' : 'asc_source_units',
1044
+ current_value: topSourceValue,
940
1045
  baseline_value: totalSourcePageViews,
941
1046
  delta_percent: round(share * 100),
942
1047
  evidence: [
943
- `Top source: ${topSource.title} (${topSource.pageViewUnique} unique product page views)`,
1048
+ `Top source: ${topSource.title} (${topSourceValue} source-attributed events)`,
944
1049
  `Source mix: ${sourceBreakdown
945
1050
  .slice(0, 5)
946
- .map((source) => `${source.title} ${source.pageViewUnique}`)
1051
+ .map((source) => `${source.title} ${source.pageViewUnique || source.impressions || source.units || source.purchases || source.proceeds || 0}`)
947
1052
  .join(', ')}`,
948
- 'ASC sources are product page views by unique devices, not source-level download units',
1053
+ 'ASC source dimensions may represent impressions, product page views, downloads, purchases, or proceeds depending on the downloaded report instance',
949
1054
  ],
950
1055
  suggested_actions: [
951
1056
  'Turn the dominant source into a specific Handlungsempfehlung: Search -> ASO/keywords, Web Referrer -> landing pages/UTMs, Browse -> creative/category positioning, App Referrer -> cross-promo/deep links',
@@ -971,27 +1076,11 @@ export function buildAscSummary(input) {
971
1076
  batchReports,
972
1077
  productionVersions,
973
1078
  analytics: {
974
- units: unitsMetric
975
- ? {
976
- total: coerceNumber(unitsMetric.total) ?? 0,
977
- previousTotal: coerceNumber(unitsMetric.previousTotal),
978
- percentChange: coerceNumber(unitsMetric.percentChange),
979
- }
980
- : null,
981
- redownloads: redownloadsMetric
982
- ? {
983
- total: coerceNumber(redownloadsMetric.total) ?? 0,
984
- previousTotal: coerceNumber(redownloadsMetric.previousTotal),
985
- percentChange: coerceNumber(redownloadsMetric.percentChange),
986
- }
987
- : null,
988
- conversionRate: conversionRateMetric
989
- ? {
990
- total: coerceNumber(conversionRateMetric.total) ?? 0,
991
- previousTotal: coerceNumber(conversionRateMetric.previousTotal),
992
- percentChange: coerceNumber(conversionRateMetric.percentChange),
993
- }
994
- : null,
1079
+ impressions: metricSnapshot(impressionsMetric),
1080
+ pageViewUnique: metricSnapshot(pageViewsMetric),
1081
+ units: metricSnapshot(unitsMetric),
1082
+ redownloads: metricSnapshot(redownloadsMetric),
1083
+ conversionRate: metricSnapshot(conversionRateMetric),
995
1084
  crashRate: crashRateMetric
996
1085
  ? {
997
1086
  total: coerceNumber(crashRateMetric.total) ?? 0,
@@ -1000,6 +1089,16 @@ export function buildAscSummary(input) {
1000
1089
  nonZeroDays: nonZeroCrashRateDays,
1001
1090
  }
1002
1091
  : null,
1092
+ crashes: metricSnapshot(crashesMetric),
1093
+ sessions: metricSnapshot(sessionsMetric),
1094
+ activeDevices: metricSnapshot(activeDevicesMetric),
1095
+ installations: metricSnapshot(installationsMetric),
1096
+ deletions: metricSnapshot(deletionsMetric),
1097
+ purchases: metricSnapshot(purchasesMetric),
1098
+ proceeds: metricSnapshot(proceedsMetric),
1099
+ payingUsers: metricSnapshot(payingUsersMetric),
1100
+ subscriptions: metricSnapshot(subscriptionsMetric),
1101
+ trialStarts: metricSnapshot(trialStartsMetric),
1003
1102
  totalCrashes,
1004
1103
  crashBreakdown,
1005
1104
  sourceBreakdown,