@yoonion/mimi-seed-mcp 0.19.0 → 0.19.2
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 +3 -3
- package/assets/agent-guide.md +6 -3
- package/dist/appstore/analytics.d.ts +140 -0
- package/dist/appstore/analytics.js +309 -0
- package/dist/checks/billing.d.ts +36 -0
- package/dist/checks/billing.js +307 -0
- package/dist/checks/risks.d.ts +4 -1
- package/dist/checks/risks.js +78 -5
- package/dist/firebase/remote-config.d.ts +163 -0
- package/dist/firebase/remote-config.js +239 -0
- package/dist/lib/googleapis-lite.d.ts +1 -0
- package/dist/lib/googleapis-lite.js +3 -0
- package/dist/registers/appstore.js +12 -0
- package/dist/registers/checks.js +16 -2
- package/dist/registers/firebase.js +13 -0
- package/package.json +1 -1
- package/tool-manifest.json +8 -5
package/README.md
CHANGED
|
@@ -79,9 +79,9 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
79
79
|
|
|
80
80
|
| 영역 | 도구 수 | 주요 도구 |
|
|
81
81
|
|------|---------|-----------|
|
|
82
|
-
| App Store Connect |
|
|
82
|
+
| App Store Connect | 64 | `appstore_submit_for_review` / `appstore_get_weekly_insight` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
|
|
83
83
|
| Google Play | 39 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
|
|
84
|
-
| Firebase |
|
|
84
|
+
| Firebase | 21 | `firebase_create_project` / `firebase_get_remote_config_overview` / `firebase_get_android_config` / `firebase_create_ios_app` |
|
|
85
85
|
| AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
|
|
86
86
|
| CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
|
|
87
87
|
| Jenkins (크리덴셜 + 잡) | 10 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_create_job` / `jenkins_update_job` |
|
|
@@ -94,7 +94,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
94
94
|
| BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
|
|
95
95
|
| Threads | 7 | `threads_post` / `threads_post_video` / `threads_post_carousel` / `threads_refresh_token` |
|
|
96
96
|
| TikTok Business | 7 | `tiktok_business_plan_video_post` / `tiktok_business_publish_video` / `tiktok_business_get_publish_status` |
|
|
97
|
-
| 점검 / 위험 |
|
|
97
|
+
| 점검 / 위험 | 5 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `android_check_billing_compliance` / `screenshot_validate` / `release_status` |
|
|
98
98
|
| Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
|
|
99
99
|
| Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
|
|
100
100
|
| 인증 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
|
package/assets/agent-guide.md
CHANGED
|
@@ -49,6 +49,7 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
49
49
|
|------|--------------------|
|
|
50
50
|
| First contact / "what's connected?" | `select:mimi_seed_status,mimi_seed_auth_status,mimi_seed_auth_start,mimi_seed_remote_sync_credentials` |
|
|
51
51
|
| Release readiness (either store) | `select:release_status,playstore_check_submission_risks,appstore_check_submission_risks,screenshot_validate` |
|
|
52
|
+
| Android Billing compliance | `select:android_check_billing_compliance,playstore_check_submission_risks` |
|
|
52
53
|
| Play Store release | `select:playstore_get_app,playstore_list_tracks,playstore_update_release_notes,playstore_update_latest_release_notes,playstore_promote_release,playstore_submit_release,playstore_check_submission_risks,playstore_plan_release` |
|
|
53
54
|
| Play Store listing + images | `select:playstore_get_listing,playstore_update_listing,playstore_update_details,playstore_upload_image,playstore_list_images,playstore_replace_images,playstore_delete_all_images` |
|
|
54
55
|
| Play Store reviews + stats | `select:playstore_list_reviews,playstore_reply_review,playstore_get_statistics,generate_review_reply` |
|
|
@@ -67,10 +68,12 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
67
68
|
| Release notes from commits | `select:generate_release_notes_from_commits,playstore_update_release_notes,appstore_update_whats_new` |
|
|
68
69
|
| Firebase setup | `select:firebase_list_projects,firebase_get_project,firebase_create_project,firebase_create_android_app,firebase_create_ios_app,firebase_get_android_config,firebase_get_ios_config,firebase_enable_common_services` |
|
|
69
70
|
| Firebase apps + services (incl. web) | `select:firebase_list_android_apps,firebase_list_ios_apps,firebase_list_web_apps,firebase_create_web_app,firebase_get_web_config,firebase_enable_service,firebase_list_enabled_services,firebase_delete_android_app,firebase_delete_ios_app,firebase_delete_web_app` |
|
|
71
|
+
| Remote Config usage + experiments | `select:firebase_get_remote_config_overview,gcp_get_billing_info` |
|
|
70
72
|
| Analytics wiring (Firebase ↔ GA4 ↔ BigQuery) | `select:firebase_link_analytics,firebase_get_analytics_details,ga4_list_account_summaries,ga4_list_properties,ga4_create_property,ga4_list_data_streams,ga4_create_data_stream,ga4_plan_bigquery_link,ga4_create_bigquery_link,ga4_run_report` |
|
|
71
73
|
| BigQuery | `select:bigquery_auth_status,bigquery_list_datasets,bigquery_list_tables,bigquery_get_table_schema,bigquery_run_query` |
|
|
72
74
|
| GCP billing (Blaze 여부 · 비용 범위 · 예산) | `select:gcp_get_billing_info,gcp_list_billing_projects,gcp_list_budgets,gcp_create_budget` |
|
|
73
75
|
| Real revenue (스토어 정산 원장 — sandbox·테스터가 섞이지 않는 유일한 창구) | `select:appstore_get_sales_report,appstore_get_finance_report,playstore_list_financial_reports,playstore_get_financial_report` |
|
|
76
|
+
| App Store weekly growth insight | `select:appstore_get_weekly_insight,appstore_get_sales_report` |
|
|
74
77
|
| AdMob | `select:admob_list_accounts,admob_list_apps,admob_create_app,admob_create_ad_unit,admob_list_ad_units,admob_get_today_earnings,admob_get_report` |
|
|
75
78
|
| Google Ads (UAC) | `select:googleads_config_status,googleads_save_config,googleads_list_accessible_customers,googleads_list_campaigns,googleads_get_campaign_report,googleads_get_uac_report` |
|
|
76
79
|
| Search Console | `select:gsc_list_sites,gsc_list_sitemaps,gsc_get_sitemap,gsc_submit_sitemap,gsc_inspect_url,gsc_search_analytics` |
|
|
@@ -176,8 +179,8 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
176
179
|
| Domain | Representative tools |
|
|
177
180
|
|--------|----------------------|
|
|
178
181
|
| **Google Play** | `playstore_get_app` · `playstore_get_listing` · `playstore_update_listing` · `playstore_list_tracks` · `playstore_update_latest_release_notes` · `playstore_promote_release` · `playstore_submit_release` · `playstore_upload_image` · `playstore_replace_images` · `playstore_check_submission_risks` · `playstore_get_statistics` · `playstore_reply_review` · `playstore_register_service_account` |
|
|
179
|
-
| **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_latest_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` |
|
|
180
|
-
| **Firebase** | `firebase_create_project` · `firebase_create_android_app` · `firebase_create_ios_app` · `firebase_get_android_config` · `firebase_enable_service` · `firebase_enable_common_services` · `firebase_list_*_apps` |
|
|
182
|
+
| **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_latest_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` · `appstore_get_weekly_insight` |
|
|
183
|
+
| **Firebase** | `firebase_create_project` · `firebase_create_android_app` · `firebase_create_ios_app` · `firebase_get_android_config` · `firebase_enable_service` · `firebase_enable_common_services` · `firebase_get_remote_config_overview` · `firebase_list_*_apps` |
|
|
181
184
|
| **AdMob** | `admob_create_app` · `admob_create_ad_unit` · `admob_list_ad_units` · `admob_get_today_earnings` · `admob_get_report` |
|
|
182
185
|
| **CI/CD** | `ci_trigger_build` · `ci_get_build_status` · `ci_list_workflows` (**GitHub Actions / GitLab only**) |
|
|
183
186
|
| **Jenkins** (credentials + jobs) | `jenkins_status` · `jenkins_save_config` · `jenkins_create_credential` · `jenkins_upload_keystore` · `jenkins_upload_playstore_sa` · `jenkins_create_job` · `jenkins_update_job` |
|
|
@@ -189,7 +192,7 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
189
192
|
| **Android signing** | `android_signing_setup` · `android_generate_keystore` · `jenkins_upload_playstore_sa` |
|
|
190
193
|
| **Facebook / Instagram / Threads** | `facebook_post_photo` · `instagram_post_carousel` · `threads_post` · `threads_post_video` · `threads_refresh_token` |
|
|
191
194
|
| **TikTok Business** | `tiktok_business_auth_status` · `tiktok_business_plan_video_post` · `tiktok_business_publish_video` · `tiktok_business_get_publish_status` |
|
|
192
|
-
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `screenshot_validate` · `release_status` |
|
|
195
|
+
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `android_check_billing_compliance` · `screenshot_validate` · `release_status` |
|
|
193
196
|
| **AI / Auth** | `generate_release_notes_from_commits` · `generate_review_reply` · `mimi_seed_status` · `mimi_seed_auth_start` · `mimi_seed_auth_status` · `mimi_seed_remote_sync_credentials` |
|
|
194
197
|
| **Video production** | `youtube_upload_video` · `youtube_get_video_status` · `youtube_update_video_privacy` · `video_save_plan` · `video_plan_from_story` · `video_research_youtube` · `video_search_stock_assets` · `video_synthesize_research` · `video_generate_image` · `video_build_timeline` · `video_render` · `video_job_status` · `video_validate` |
|
|
195
198
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type ReportRow } from './sales.js';
|
|
2
|
+
interface JsonApiRow<T> {
|
|
3
|
+
id: string;
|
|
4
|
+
attributes?: T;
|
|
5
|
+
}
|
|
6
|
+
interface AnalyticsReportAttributes {
|
|
7
|
+
name?: string;
|
|
8
|
+
category?: 'APP_USAGE' | 'APP_STORE_ENGAGEMENT' | 'COMMERCE' | 'FRAMEWORK_USAGE' | 'PERFORMANCE';
|
|
9
|
+
}
|
|
10
|
+
export declare function selectWeeklyAnalyticsReports(reports: Array<JsonApiRow<AnalyticsReportAttributes>>): {
|
|
11
|
+
engagement: JsonApiRow<AnalyticsReportAttributes> | undefined;
|
|
12
|
+
downloads: JsonApiRow<AnalyticsReportAttributes> | undefined;
|
|
13
|
+
purchases: JsonApiRow<AnalyticsReportAttributes> | undefined;
|
|
14
|
+
};
|
|
15
|
+
interface WeeklyMetrics {
|
|
16
|
+
date: string;
|
|
17
|
+
impressions: number;
|
|
18
|
+
productPageViews: number;
|
|
19
|
+
downloads: number;
|
|
20
|
+
proceeds: number;
|
|
21
|
+
available: {
|
|
22
|
+
engagement: boolean;
|
|
23
|
+
downloads: boolean;
|
|
24
|
+
purchases: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function selectLatestInstanceRows(batches: Array<{
|
|
28
|
+
processingDate: string;
|
|
29
|
+
rows: ReportRow[];
|
|
30
|
+
}>): ReportRow[];
|
|
31
|
+
export declare function buildWeeklyInsight(input: {
|
|
32
|
+
engagement: ReportRow[];
|
|
33
|
+
downloads: ReportRow[];
|
|
34
|
+
purchases: ReportRow[];
|
|
35
|
+
}): {
|
|
36
|
+
status: string;
|
|
37
|
+
weeks: WeeklyMetrics[];
|
|
38
|
+
recommendation: string;
|
|
39
|
+
current?: undefined;
|
|
40
|
+
previous?: undefined;
|
|
41
|
+
changes?: undefined;
|
|
42
|
+
insight?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
status: string;
|
|
45
|
+
current: WeeklyMetrics;
|
|
46
|
+
previous: WeeklyMetrics;
|
|
47
|
+
changes: {
|
|
48
|
+
impressions: number | null;
|
|
49
|
+
productPageViews: number | null;
|
|
50
|
+
downloads: number | null;
|
|
51
|
+
proceeds: number | null;
|
|
52
|
+
productPageRate: number | null;
|
|
53
|
+
downloadRate: number | null;
|
|
54
|
+
revenuePerDownload: number | null;
|
|
55
|
+
};
|
|
56
|
+
insight: {
|
|
57
|
+
area: string;
|
|
58
|
+
changePercent: number | null;
|
|
59
|
+
trend: string;
|
|
60
|
+
recommendation: string;
|
|
61
|
+
} | {
|
|
62
|
+
area: string;
|
|
63
|
+
changePercent: null;
|
|
64
|
+
recommendation: string;
|
|
65
|
+
trend?: undefined;
|
|
66
|
+
};
|
|
67
|
+
weeks?: undefined;
|
|
68
|
+
recommendation?: undefined;
|
|
69
|
+
};
|
|
70
|
+
export declare function getWeeklyInsight(input: {
|
|
71
|
+
appId: string;
|
|
72
|
+
createIfMissing?: boolean;
|
|
73
|
+
confirmCreate?: boolean;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
status: string;
|
|
76
|
+
message: string;
|
|
77
|
+
nextAction: string;
|
|
78
|
+
requestId?: undefined;
|
|
79
|
+
} | {
|
|
80
|
+
status: string;
|
|
81
|
+
message: string;
|
|
82
|
+
nextAction?: undefined;
|
|
83
|
+
requestId?: undefined;
|
|
84
|
+
} | {
|
|
85
|
+
status: string;
|
|
86
|
+
requestId: string;
|
|
87
|
+
message: string;
|
|
88
|
+
nextAction?: undefined;
|
|
89
|
+
} | {
|
|
90
|
+
status: string;
|
|
91
|
+
weeks: WeeklyMetrics[];
|
|
92
|
+
recommendation: string;
|
|
93
|
+
current?: undefined;
|
|
94
|
+
previous?: undefined;
|
|
95
|
+
changes?: undefined;
|
|
96
|
+
insight?: undefined;
|
|
97
|
+
requestId: string;
|
|
98
|
+
reports: {
|
|
99
|
+
engagement: string | null;
|
|
100
|
+
downloads: string | null;
|
|
101
|
+
purchases: string | null;
|
|
102
|
+
};
|
|
103
|
+
message?: undefined;
|
|
104
|
+
nextAction?: undefined;
|
|
105
|
+
} | {
|
|
106
|
+
status: string;
|
|
107
|
+
current: WeeklyMetrics;
|
|
108
|
+
previous: WeeklyMetrics;
|
|
109
|
+
changes: {
|
|
110
|
+
impressions: number | null;
|
|
111
|
+
productPageViews: number | null;
|
|
112
|
+
downloads: number | null;
|
|
113
|
+
proceeds: number | null;
|
|
114
|
+
productPageRate: number | null;
|
|
115
|
+
downloadRate: number | null;
|
|
116
|
+
revenuePerDownload: number | null;
|
|
117
|
+
};
|
|
118
|
+
insight: {
|
|
119
|
+
area: string;
|
|
120
|
+
changePercent: number | null;
|
|
121
|
+
trend: string;
|
|
122
|
+
recommendation: string;
|
|
123
|
+
} | {
|
|
124
|
+
area: string;
|
|
125
|
+
changePercent: null;
|
|
126
|
+
recommendation: string;
|
|
127
|
+
trend?: undefined;
|
|
128
|
+
};
|
|
129
|
+
weeks?: undefined;
|
|
130
|
+
recommendation?: undefined;
|
|
131
|
+
requestId: string;
|
|
132
|
+
reports: {
|
|
133
|
+
engagement: string | null;
|
|
134
|
+
downloads: string | null;
|
|
135
|
+
purchases: string | null;
|
|
136
|
+
};
|
|
137
|
+
message?: undefined;
|
|
138
|
+
nextAction?: undefined;
|
|
139
|
+
}>;
|
|
140
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import zlib from 'node:zlib';
|
|
3
|
+
import { getReportsAuthHeaders } from './auth.js';
|
|
4
|
+
import { V1_BASE, apiRequest, authHeadersOrThrow } from './http.js';
|
|
5
|
+
import { fetchWithTimeout, HTTP_TRANSFER_TIMEOUT_MS } from '../lib/http.js';
|
|
6
|
+
import { parseTsv } from './sales.js';
|
|
7
|
+
export function selectWeeklyAnalyticsReports(reports) {
|
|
8
|
+
const standard = reports.filter((row) => !/Detailed/i.test(row.attributes?.name ?? ''));
|
|
9
|
+
const detailed = reports.filter((row) => /Detailed/i.test(row.attributes?.name ?? ''));
|
|
10
|
+
return {
|
|
11
|
+
engagement: standard.find((row) => /Discovery and Engagement/i.test(row.attributes?.name ?? '')),
|
|
12
|
+
// Apple only exposes weekly App Store Downloads as a detailed report. Selecting the
|
|
13
|
+
// standard report here silently yields no WEEKLY instances even when analytics is ready.
|
|
14
|
+
downloads: detailed.find((row) => /App Store Downloads/i.test(row.attributes?.name ?? ''))
|
|
15
|
+
?? standard.find((row) => /App Store Downloads/i.test(row.attributes?.name ?? '')),
|
|
16
|
+
purchases: standard.find((row) => /App Store Purchases/i.test(row.attributes?.name ?? '')),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const MAX_SEGMENT_BYTES = 50 * 1024 * 1024;
|
|
20
|
+
const MAX_DECOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
21
|
+
const MAX_WEEKLY_INSTANCES = 8;
|
|
22
|
+
async function reportHeaders() {
|
|
23
|
+
const auth = await getReportsAuthHeaders();
|
|
24
|
+
if (!auth)
|
|
25
|
+
throw new Error('App Store Connect reportsKey 또는 기본 API 키가 필요합니다.');
|
|
26
|
+
return auth.headers;
|
|
27
|
+
}
|
|
28
|
+
async function reportGet(path, params) {
|
|
29
|
+
const query = params ? `?${new URLSearchParams(params).toString()}` : '';
|
|
30
|
+
return apiRequest(V1_BASE, `${path}${query}`, await reportHeaders(), { method: 'GET' });
|
|
31
|
+
}
|
|
32
|
+
async function createOngoingRequest(appId) {
|
|
33
|
+
const response = await apiRequest(V1_BASE, '/analyticsReportRequests', await authHeadersOrThrow(), {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
data: {
|
|
38
|
+
type: 'analyticsReportRequests',
|
|
39
|
+
attributes: { accessType: 'ONGOING' },
|
|
40
|
+
relationships: { app: { data: { type: 'apps', id: appId } } },
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
async function downloadSegment(segment) {
|
|
47
|
+
const attributes = segment.attributes ?? {};
|
|
48
|
+
if (!attributes.url)
|
|
49
|
+
return [];
|
|
50
|
+
if ((attributes.sizeInBytes ?? 0) > MAX_SEGMENT_BYTES) {
|
|
51
|
+
throw new Error(`Analytics segment ${segment.id} is larger than the 50 MB safety limit.`);
|
|
52
|
+
}
|
|
53
|
+
const response = await fetchWithTimeout(attributes.url, {}, HTTP_TRANSFER_TIMEOUT_MS);
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
throw new Error(`Analytics segment download failed (${response.status}).`);
|
|
56
|
+
const contentLength = Number(response.headers.get('content-length') ?? 0);
|
|
57
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_SEGMENT_BYTES) {
|
|
58
|
+
throw new Error(`Analytics segment ${segment.id} response is larger than the 50 MB safety limit.`);
|
|
59
|
+
}
|
|
60
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
61
|
+
if (bytes.length > MAX_SEGMENT_BYTES) {
|
|
62
|
+
throw new Error(`Analytics segment ${segment.id} downloaded more than the 50 MB safety limit.`);
|
|
63
|
+
}
|
|
64
|
+
if (attributes.checksum) {
|
|
65
|
+
const actual = crypto.createHash('md5').update(bytes).digest('hex');
|
|
66
|
+
if (actual !== attributes.checksum.toLowerCase()) {
|
|
67
|
+
throw new Error(`Analytics segment checksum mismatch for ${segment.id}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const isGzip = bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
71
|
+
const text = isGzip
|
|
72
|
+
? zlib.gunzipSync(bytes, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString('utf8')
|
|
73
|
+
: bytes.toString('utf8');
|
|
74
|
+
return parseTsv(text);
|
|
75
|
+
}
|
|
76
|
+
export function selectLatestInstanceRows(batches) {
|
|
77
|
+
const latestByDate = new Map();
|
|
78
|
+
for (const batch of batches) {
|
|
79
|
+
const rowsByDate = new Map();
|
|
80
|
+
for (const row of batch.rows) {
|
|
81
|
+
const date = dateOf(row);
|
|
82
|
+
if (!date)
|
|
83
|
+
continue;
|
|
84
|
+
const rows = rowsByDate.get(date) ?? [];
|
|
85
|
+
rows.push(row);
|
|
86
|
+
rowsByDate.set(date, rows);
|
|
87
|
+
}
|
|
88
|
+
for (const [date, rows] of rowsByDate) {
|
|
89
|
+
const current = latestByDate.get(date);
|
|
90
|
+
if (!current || batch.processingDate > current.processingDate) {
|
|
91
|
+
latestByDate.set(date, { processingDate: batch.processingDate, rows });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...latestByDate.entries()]
|
|
96
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
97
|
+
.flatMap(([, value]) => value.rows);
|
|
98
|
+
}
|
|
99
|
+
async function rowsForReport(reportId) {
|
|
100
|
+
const instances = await reportGet(`/analyticsReports/${reportId}/instances`, { 'filter[granularity]': 'WEEKLY', limit: '200' });
|
|
101
|
+
const latest = (instances.data ?? [])
|
|
102
|
+
.filter((row) => row.attributes?.processingDate)
|
|
103
|
+
.sort((a, b) => (b.attributes?.processingDate ?? '').localeCompare(a.attributes?.processingDate ?? ''))
|
|
104
|
+
.slice(0, MAX_WEEKLY_INSTANCES);
|
|
105
|
+
const batches = [];
|
|
106
|
+
for (const instance of latest) {
|
|
107
|
+
const segments = await reportGet(`/analyticsReportInstances/${instance.id}/segments`, { limit: '200' });
|
|
108
|
+
const rows = [];
|
|
109
|
+
for (const segment of segments.data ?? [])
|
|
110
|
+
rows.push(...await downloadSegment(segment));
|
|
111
|
+
batches.push({ processingDate: instance.attributes.processingDate, rows });
|
|
112
|
+
}
|
|
113
|
+
return selectLatestInstanceRows(batches);
|
|
114
|
+
}
|
|
115
|
+
function numberFrom(row, names) {
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
const raw = row[name];
|
|
118
|
+
if (raw === undefined || raw === '')
|
|
119
|
+
continue;
|
|
120
|
+
const value = Number.parseFloat(raw.replace(/,/g, ''));
|
|
121
|
+
if (Number.isFinite(value))
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
function dateOf(row) {
|
|
127
|
+
return row.Date || row['Start Date'] || row['Week Start Date'] || null;
|
|
128
|
+
}
|
|
129
|
+
function ensureWeek(map, date) {
|
|
130
|
+
let week = map.get(date);
|
|
131
|
+
if (!week) {
|
|
132
|
+
week = {
|
|
133
|
+
date,
|
|
134
|
+
impressions: 0,
|
|
135
|
+
productPageViews: 0,
|
|
136
|
+
downloads: 0,
|
|
137
|
+
proceeds: 0,
|
|
138
|
+
available: { engagement: false, downloads: false, purchases: false },
|
|
139
|
+
};
|
|
140
|
+
map.set(date, week);
|
|
141
|
+
}
|
|
142
|
+
return week;
|
|
143
|
+
}
|
|
144
|
+
function percentageChange(current, previous) {
|
|
145
|
+
if (previous <= 0)
|
|
146
|
+
return null;
|
|
147
|
+
return ((current - previous) / previous) * 100;
|
|
148
|
+
}
|
|
149
|
+
export function buildWeeklyInsight(input) {
|
|
150
|
+
const weeks = new Map();
|
|
151
|
+
for (const row of input.engagement) {
|
|
152
|
+
const date = dateOf(row);
|
|
153
|
+
if (!date)
|
|
154
|
+
continue;
|
|
155
|
+
const week = ensureWeek(weeks, date);
|
|
156
|
+
week.available.engagement = true;
|
|
157
|
+
const event = (row.Event ?? '').toLowerCase();
|
|
158
|
+
const pageType = (row['Page Type'] ?? '').toLowerCase();
|
|
159
|
+
const count = numberFrom(row, ['Unique Counts', 'Counts', 'Count']);
|
|
160
|
+
if (event === 'impression')
|
|
161
|
+
week.impressions += count;
|
|
162
|
+
if (event === 'page view' && (pageType === 'product page' || !pageType))
|
|
163
|
+
week.productPageViews += count;
|
|
164
|
+
}
|
|
165
|
+
for (const row of input.downloads) {
|
|
166
|
+
const date = dateOf(row);
|
|
167
|
+
if (!date)
|
|
168
|
+
continue;
|
|
169
|
+
const week = ensureWeek(weeks, date);
|
|
170
|
+
week.available.downloads = true;
|
|
171
|
+
week.downloads += numberFrom(row, ['Counts', 'Count', 'Downloads', 'Units']);
|
|
172
|
+
}
|
|
173
|
+
for (const row of input.purchases) {
|
|
174
|
+
const date = dateOf(row);
|
|
175
|
+
if (!date)
|
|
176
|
+
continue;
|
|
177
|
+
const week = ensureWeek(weeks, date);
|
|
178
|
+
week.available.purchases = true;
|
|
179
|
+
week.proceeds += numberFrom(row, [
|
|
180
|
+
'Proceeds in USD',
|
|
181
|
+
'Developer Proceeds',
|
|
182
|
+
'Proceeds',
|
|
183
|
+
'Estimated Proceeds',
|
|
184
|
+
'Sales',
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
const ordered = [...weeks.values()]
|
|
188
|
+
.filter((week) => week.available.engagement && week.available.downloads)
|
|
189
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
190
|
+
if (ordered.length < 2) {
|
|
191
|
+
return {
|
|
192
|
+
status: 'collecting',
|
|
193
|
+
weeks: ordered,
|
|
194
|
+
recommendation: 'Analytics 주간 데이터가 두 기간 이상 쌓인 뒤 다시 실행하세요.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const previous = ordered.at(-2);
|
|
198
|
+
const current = ordered.at(-1);
|
|
199
|
+
const previousPageRate = previous.impressions > 0 ? previous.productPageViews / previous.impressions : 0;
|
|
200
|
+
const currentPageRate = current.impressions > 0 ? current.productPageViews / current.impressions : 0;
|
|
201
|
+
const previousDownloadRate = previous.productPageViews > 0 ? previous.downloads / previous.productPageViews : 0;
|
|
202
|
+
const currentDownloadRate = current.productPageViews > 0 ? current.downloads / current.productPageViews : 0;
|
|
203
|
+
const previousRevenuePerDownload = previous.downloads > 0 ? previous.proceeds / previous.downloads : 0;
|
|
204
|
+
const currentRevenuePerDownload = current.downloads > 0 ? current.proceeds / current.downloads : 0;
|
|
205
|
+
const candidates = [
|
|
206
|
+
{
|
|
207
|
+
area: 'product_page',
|
|
208
|
+
changePercent: percentageChange(currentPageRate, previousPageRate),
|
|
209
|
+
declineRecommendation: '제품 페이지 진입률이 하락했습니다. 아이콘·스크린샷 첫 장·부제 중 하나를 바꿔 Product Page Optimization 테스트를 시작하세요.',
|
|
210
|
+
healthyRecommendation: '핵심 전환 지표의 주간 하락은 없습니다. 가장 개선 폭이 작은 제품 페이지 진입률을 다음 실험 대상으로 삼으세요.',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
area: 'acquisition',
|
|
214
|
+
changePercent: percentageChange(currentDownloadRate, previousDownloadRate),
|
|
215
|
+
declineRecommendation: '제품 페이지 조회 대비 다운로드 전환이 하락했습니다. 유입 소스별 전환을 나누고 가장 큰 하락 소스에 맞춘 커스텀 제품 페이지를 만드세요.',
|
|
216
|
+
healthyRecommendation: '핵심 전환 지표의 주간 하락은 없습니다. 가장 개선 폭이 작은 다운로드 전환을 유입 소스별로 나눠 다음 실험을 정하세요.',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
area: 'monetization',
|
|
220
|
+
changePercent: previous.available.purchases && current.available.purchases
|
|
221
|
+
? percentageChange(currentRevenuePerDownload, previousRevenuePerDownload)
|
|
222
|
+
: null,
|
|
223
|
+
declineRecommendation: '다운로드당 수익이 하락했습니다. 구매 리포트에서 상품별 하락을 확인하고 가격·오퍼·구독 전환 중 한 가지를 실험하세요.',
|
|
224
|
+
healthyRecommendation: '핵심 전환 지표의 주간 하락은 없습니다. 가장 개선 폭이 작은 다운로드당 수익을 상품별로 나눠 다음 가격·오퍼 실험을 정하세요.',
|
|
225
|
+
},
|
|
226
|
+
].filter((candidate) => candidate.changePercent !== null)
|
|
227
|
+
.sort((a, b) => (a.changePercent ?? 0) - (b.changePercent ?? 0));
|
|
228
|
+
const selected = candidates[0];
|
|
229
|
+
return {
|
|
230
|
+
status: 'ready',
|
|
231
|
+
current,
|
|
232
|
+
previous,
|
|
233
|
+
changes: {
|
|
234
|
+
impressions: percentageChange(current.impressions, previous.impressions),
|
|
235
|
+
productPageViews: percentageChange(current.productPageViews, previous.productPageViews),
|
|
236
|
+
downloads: percentageChange(current.downloads, previous.downloads),
|
|
237
|
+
proceeds: previous.available.purchases && current.available.purchases
|
|
238
|
+
? percentageChange(current.proceeds, previous.proceeds)
|
|
239
|
+
: null,
|
|
240
|
+
productPageRate: percentageChange(currentPageRate, previousPageRate),
|
|
241
|
+
downloadRate: percentageChange(currentDownloadRate, previousDownloadRate),
|
|
242
|
+
revenuePerDownload: previous.available.purchases && current.available.purchases
|
|
243
|
+
? percentageChange(currentRevenuePerDownload, previousRevenuePerDownload)
|
|
244
|
+
: null,
|
|
245
|
+
},
|
|
246
|
+
insight: selected
|
|
247
|
+
? {
|
|
248
|
+
area: selected.area,
|
|
249
|
+
changePercent: selected.changePercent,
|
|
250
|
+
trend: (selected.changePercent ?? 0) < 0 ? 'declining' : 'stable_or_improving',
|
|
251
|
+
recommendation: (selected.changePercent ?? 0) < 0
|
|
252
|
+
? selected.declineRecommendation
|
|
253
|
+
: selected.healthyRecommendation,
|
|
254
|
+
}
|
|
255
|
+
: {
|
|
256
|
+
area: 'data_quality',
|
|
257
|
+
changePercent: null,
|
|
258
|
+
recommendation: '비교 가능한 분모 데이터가 부족합니다. Engagement·주간 Downloads Detailed·Purchases 리포트가 생성되는지 확인하세요.',
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
export async function getWeeklyInsight(input) {
|
|
263
|
+
const requests = await reportGet(`/apps/${input.appId}/analyticsReportRequests`, { 'filter[accessType]': 'ONGOING', limit: '10' });
|
|
264
|
+
const active = (requests.data ?? []).find((row) => !row.attributes?.stoppedDueToInactivity);
|
|
265
|
+
if (!active) {
|
|
266
|
+
if (!input.createIfMissing) {
|
|
267
|
+
return {
|
|
268
|
+
status: 'setup_required',
|
|
269
|
+
message: 'ONGOING Analytics report request가 없습니다.',
|
|
270
|
+
nextAction: 'createIfMissing=true와 confirmCreate=true로 다시 호출하세요.',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (!input.confirmCreate) {
|
|
274
|
+
return {
|
|
275
|
+
status: 'confirmation_required',
|
|
276
|
+
message: 'Analytics report request 생성은 App Store Connect에 지속 리소스를 만듭니다.',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const created = await createOngoingRequest(input.appId);
|
|
280
|
+
return {
|
|
281
|
+
status: 'provisioning',
|
|
282
|
+
requestId: created.id,
|
|
283
|
+
message: 'ONGOING Analytics report request를 만들었습니다. 최초 데이터는 보통 24~48시간 뒤 생성됩니다.',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const reports = await reportGet(`/analyticsReportRequests/${active.id}/reports`, { limit: '200' });
|
|
287
|
+
const { engagement, downloads, purchases } = selectWeeklyAnalyticsReports(reports.data ?? []);
|
|
288
|
+
if (!engagement && !downloads && !purchases) {
|
|
289
|
+
return {
|
|
290
|
+
status: 'collecting',
|
|
291
|
+
requestId: active.id,
|
|
292
|
+
message: '요청은 활성 상태지만 주간 Engagement·Downloads·Purchases 리포트가 아직 생성되지 않았습니다.',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const [engagementRows, downloadRows, purchaseRows] = await Promise.all([
|
|
296
|
+
engagement ? rowsForReport(engagement.id) : [],
|
|
297
|
+
downloads ? rowsForReport(downloads.id) : [],
|
|
298
|
+
purchases ? rowsForReport(purchases.id) : [],
|
|
299
|
+
]);
|
|
300
|
+
return {
|
|
301
|
+
requestId: active.id,
|
|
302
|
+
reports: {
|
|
303
|
+
engagement: engagement?.attributes?.name ?? null,
|
|
304
|
+
downloads: downloads?.attributes?.name ?? null,
|
|
305
|
+
purchases: purchases?.attributes?.name ?? null,
|
|
306
|
+
},
|
|
307
|
+
...buildWeeklyInsight({ engagement: engagementRows, downloads: downloadRows, purchases: purchaseRows }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type BillingComplianceStatus = 'pass' | 'warning' | 'blocker' | 'unresolved' | 'not_used';
|
|
2
|
+
export interface BillingEvidence {
|
|
3
|
+
file: string;
|
|
4
|
+
module: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
expression?: string;
|
|
7
|
+
source: 'literal' | 'variable' | 'version_catalog' | 'unresolved';
|
|
8
|
+
}
|
|
9
|
+
export interface BillingComplianceResult {
|
|
10
|
+
projectPath: string;
|
|
11
|
+
checkedAt: string;
|
|
12
|
+
status: BillingComplianceStatus;
|
|
13
|
+
detectedVersions: string[];
|
|
14
|
+
evidence: BillingEvidence[];
|
|
15
|
+
policy: {
|
|
16
|
+
minimumSupportedMajor: number | null;
|
|
17
|
+
submissionDeadline: string;
|
|
18
|
+
extensionDeadline: string;
|
|
19
|
+
latestKnownMajor: number;
|
|
20
|
+
scheduleCurrent: boolean;
|
|
21
|
+
knownSchedule: Array<{
|
|
22
|
+
major: number;
|
|
23
|
+
submissionDeadline: string;
|
|
24
|
+
extensionDeadline: string;
|
|
25
|
+
}>;
|
|
26
|
+
sourceUrl: string;
|
|
27
|
+
};
|
|
28
|
+
summary: string;
|
|
29
|
+
actions: string[];
|
|
30
|
+
upgrade: {
|
|
31
|
+
installCommand: string;
|
|
32
|
+
prompt: string;
|
|
33
|
+
automaticExecution: false;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function checkBillingCompliance(projectPath: string, now?: Date): Promise<BillingComplianceResult>;
|