@rawdash/connector-google-play-console 0.24.0 → 0.26.0
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 +18 -18
- package/dist/index.d.ts +126 -121
- package/dist/index.js +213 -93
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
defineConfigFields,
|
|
12
12
|
defineConnectorDoc,
|
|
13
13
|
defineResources,
|
|
14
|
-
schemasFromResources
|
|
14
|
+
schemasFromResources,
|
|
15
|
+
selectActivePhases
|
|
15
16
|
} from "@rawdash/core";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
var configFields = defineConfigFields(
|
|
@@ -32,6 +33,11 @@ var configFields = defineConfigFields(
|
|
|
32
33
|
label: "Lookback days (full sync)",
|
|
33
34
|
description: "How many calendar days to fetch on a full sync. Defaults to 30. The Play Developer Reporting API exposes daily metrics with a typical 2-3 day reporting lag.",
|
|
34
35
|
placeholder: "30"
|
|
36
|
+
}),
|
|
37
|
+
reviewLimit: z.number().int().positive().max(2e3).optional().meta({
|
|
38
|
+
label: "Review sample size",
|
|
39
|
+
description: "How many of the most-recent user reviews to emit as gplay_app_ratings samples. Defaults to 200. Reviews are fetched then ranked newest-first before this cap is applied. The Android Publisher reviews API only surfaces reviews from roughly the past week, so this is a rolling sample, not a full history.",
|
|
40
|
+
placeholder: "200"
|
|
35
41
|
})
|
|
36
42
|
})
|
|
37
43
|
);
|
|
@@ -39,7 +45,7 @@ var doc = defineConnectorDoc({
|
|
|
39
45
|
displayName: "Google Play Console",
|
|
40
46
|
category: "engineering",
|
|
41
47
|
brandColor: "#34A853",
|
|
42
|
-
tagline: "Sync daily Android app vitals from the Play Developer Reporting API
|
|
48
|
+
tagline: "Sync daily Android app vitals from the Play Developer Reporting API (crash rate, ANR rate, error counts) plus user review ratings from the Android Publisher API.",
|
|
43
49
|
vendor: {
|
|
44
50
|
name: "Google Play Console",
|
|
45
51
|
domain: "play.google.com",
|
|
@@ -50,7 +56,7 @@ var doc = defineConnectorDoc({
|
|
|
50
56
|
summary: "Authenticate against the Play Developer Reporting API and the Android Publisher API with a Google service account JSON key. The service account must be linked to your Play Console developer account.",
|
|
51
57
|
setup: [
|
|
52
58
|
"In Google Cloud, create a service account at IAM & Admin -> Service Accounts and download a JSON key.",
|
|
53
|
-
'Enable both the "Google Play
|
|
59
|
+
'Enable both the "Google Play Developer Reporting API" and the "Google Play Android Developer API" on the Cloud project.',
|
|
54
60
|
'In Google Play Console open Setup -> API access, link the same Cloud project, then invite the service account email and grant it at least the "View app information and download bulk reports" permission for the app you want to sync.',
|
|
55
61
|
'Store the service account JSON as a secret and reference it as serviceAccountJson: secret("GPLAY_SA_JSON").',
|
|
56
62
|
"Set packageName to the reverse-DNS application id of the app (e.g. com.example.app)."
|
|
@@ -58,7 +64,9 @@ var doc = defineConnectorDoc({
|
|
|
58
64
|
},
|
|
59
65
|
rateLimit: "The Play Developer Reporting API enforces a per-project quota (default 60 requests per minute); 429 responses are retried with exponential backoff.",
|
|
60
66
|
limitations: [
|
|
61
|
-
"Daily vitals (crash rate, ANR rate,
|
|
67
|
+
"Daily vitals (crash rate, ANR rate, error counts) have a 2-3 day reporting lag on the Play Developer Reporting API; incremental syncs refetch the trailing 3 days. Metric days are reported on the America/Los_Angeles calendar, the only timezone the API supports for daily aggregation.",
|
|
68
|
+
"gplay_app_ratings is a rolling sample of recent reviews from the Android Publisher reviews API (default 200, configurable via reviewLimit). Each sample carries one review with its star rating (1-5) as the value; this is not the lifetime average shown on the Play Store, and the reviews API only surfaces reviews from roughly the past week.",
|
|
69
|
+
"The apps entity carries only the configured package name; the Play Store listing title is available solely through an Android Publisher edit, which this connector does not create.",
|
|
62
70
|
"Install counts and earnings are not exposed through the Reporting API - Google delivers them only as monthly CSV reports in a private Cloud Storage bucket. Those metrics are out of scope for this connector and will land in a follow-up."
|
|
63
71
|
]
|
|
64
72
|
});
|
|
@@ -72,8 +80,8 @@ var PHASE_ORDER = [
|
|
|
72
80
|
"apps",
|
|
73
81
|
"crash_rate",
|
|
74
82
|
"anr_rate",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
83
|
+
"errors",
|
|
84
|
+
"reviews"
|
|
77
85
|
];
|
|
78
86
|
var GPLAY_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
79
87
|
function isGplayDateString(value) {
|
|
@@ -104,33 +112,42 @@ var METRIC_PHASE_CONFIGS = {
|
|
|
104
112
|
metricSet: "crashRateMetricSet",
|
|
105
113
|
metrics: ["crashRate", "distinctUsers"],
|
|
106
114
|
metricName: "gplay_crash_rate_by_day",
|
|
107
|
-
primaryMetric: "crashRate"
|
|
115
|
+
primaryMetric: "crashRate",
|
|
116
|
+
responseTag: "crash_rate"
|
|
108
117
|
},
|
|
109
118
|
anr_rate: {
|
|
110
119
|
metricSet: "anrRateMetricSet",
|
|
111
120
|
metrics: ["anrRate", "distinctUsers"],
|
|
112
121
|
metricName: "gplay_anr_rate_by_day",
|
|
113
|
-
primaryMetric: "anrRate"
|
|
114
|
-
|
|
115
|
-
ratings: {
|
|
116
|
-
metricSet: "ratingsMetricSet",
|
|
117
|
-
metrics: ["averageRating", "ratingsCount"],
|
|
118
|
-
metricName: "gplay_ratings_by_day",
|
|
119
|
-
primaryMetric: "averageRating"
|
|
122
|
+
primaryMetric: "anrRate",
|
|
123
|
+
responseTag: "anr_rate"
|
|
120
124
|
},
|
|
121
125
|
errors: {
|
|
122
126
|
metricSet: "errorCountMetricSet",
|
|
123
127
|
metrics: ["errorReportCount", "distinctUsers"],
|
|
124
128
|
metricName: "gplay_error_count_by_day",
|
|
125
|
-
primaryMetric: "errorReportCount"
|
|
129
|
+
primaryMetric: "errorReportCount",
|
|
130
|
+
responseTag: "errors"
|
|
126
131
|
}
|
|
127
132
|
};
|
|
133
|
+
var GPLAY_APP_RATINGS_METRIC = "gplay_app_ratings";
|
|
134
|
+
var RESOURCE_TO_PHASE = {
|
|
135
|
+
apps: "apps",
|
|
136
|
+
gplay_crash_rate_by_day: "crash_rate",
|
|
137
|
+
gplay_anr_rate_by_day: "anr_rate",
|
|
138
|
+
gplay_error_count_by_day: "errors",
|
|
139
|
+
[GPLAY_APP_RATINGS_METRIC]: "reviews"
|
|
140
|
+
};
|
|
128
141
|
var SCOPES = [
|
|
129
142
|
"https://www.googleapis.com/auth/playdeveloperreporting",
|
|
130
143
|
"https://www.googleapis.com/auth/androidpublisher"
|
|
131
144
|
].join(" ");
|
|
132
145
|
var REPORTING_BASE = "https://playdeveloperreporting.googleapis.com";
|
|
133
146
|
var PUBLISHER_BASE = "https://androidpublisher.googleapis.com";
|
|
147
|
+
var DAILY_TIME_ZONE = "America/Los_Angeles";
|
|
148
|
+
var DEFAULT_REVIEW_LIMIT = 200;
|
|
149
|
+
var REVIEWS_PAGE_SIZE = 100;
|
|
150
|
+
var MAX_REVIEW_PAGES = 50;
|
|
134
151
|
function base64urlFromBytes(bytes) {
|
|
135
152
|
let binary = "";
|
|
136
153
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -197,11 +214,14 @@ async function buildServiceAccountJwt(serviceAccountJson) {
|
|
|
197
214
|
body
|
|
198
215
|
};
|
|
199
216
|
}
|
|
217
|
+
var gplayDateFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
218
|
+
timeZone: DAILY_TIME_ZONE,
|
|
219
|
+
year: "numeric",
|
|
220
|
+
month: "2-digit",
|
|
221
|
+
day: "2-digit"
|
|
222
|
+
});
|
|
200
223
|
function toGplayDate(date) {
|
|
201
|
-
|
|
202
|
-
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
203
|
-
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
204
|
-
return `${y}-${m}-${d}`;
|
|
224
|
+
return gplayDateFormatter.format(date);
|
|
205
225
|
}
|
|
206
226
|
function gplayDateToMs(gplayDate) {
|
|
207
227
|
const y = gplayDate.slice(0, 4);
|
|
@@ -269,6 +289,53 @@ function rowToMetricSample(row, metricsToCollect, metricName, primaryMetric, pac
|
|
|
269
289
|
attributes
|
|
270
290
|
};
|
|
271
291
|
}
|
|
292
|
+
function timestampToMs(ts) {
|
|
293
|
+
if (!ts || typeof ts.seconds !== "string") {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const seconds = Number(ts.seconds);
|
|
297
|
+
if (!Number.isFinite(seconds)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const nanos = typeof ts.nanos === "number" ? ts.nanos : 0;
|
|
301
|
+
return Math.round(seconds * 1e3 + nanos / 1e6);
|
|
302
|
+
}
|
|
303
|
+
function reviewToRatingSample(review, packageName) {
|
|
304
|
+
const userComment = review.comments?.find((c) => c.userComment)?.userComment;
|
|
305
|
+
if (!userComment) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const rating = userComment.starRating;
|
|
309
|
+
if (typeof rating !== "number" || rating < 1 || rating > 5) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const ts = timestampToMs(userComment.lastModified);
|
|
313
|
+
if (ts === null) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const attributes = {
|
|
317
|
+
package_name: packageName,
|
|
318
|
+
review_id: review.reviewId ?? ""
|
|
319
|
+
};
|
|
320
|
+
if (userComment.reviewerLanguage) {
|
|
321
|
+
attributes["reviewer_language"] = userComment.reviewerLanguage;
|
|
322
|
+
}
|
|
323
|
+
if (userComment.device) {
|
|
324
|
+
attributes["device"] = userComment.device;
|
|
325
|
+
}
|
|
326
|
+
if (userComment.appVersionName) {
|
|
327
|
+
attributes["app_version_name"] = userComment.appVersionName;
|
|
328
|
+
}
|
|
329
|
+
if (typeof userComment.androidOsVersion === "number") {
|
|
330
|
+
attributes["android_os_version"] = userComment.androidOsVersion;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
name: GPLAY_APP_RATINGS_METRIC,
|
|
334
|
+
ts,
|
|
335
|
+
value: rating,
|
|
336
|
+
attributes
|
|
337
|
+
};
|
|
338
|
+
}
|
|
272
339
|
var dateOnlyTimeline = z.object({
|
|
273
340
|
startTime: z.object({
|
|
274
341
|
year: z.number().int(),
|
|
@@ -292,38 +359,43 @@ function metricSetSchema() {
|
|
|
292
359
|
nextPageToken: z.string().optional()
|
|
293
360
|
});
|
|
294
361
|
}
|
|
295
|
-
var
|
|
296
|
-
|
|
297
|
-
listings: z.array(
|
|
362
|
+
var reviewsResponseSchema = z.object({
|
|
363
|
+
reviews: z.array(
|
|
298
364
|
z.object({
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
365
|
+
reviewId: z.string().optional(),
|
|
366
|
+
authorName: z.string().optional(),
|
|
367
|
+
comments: z.array(
|
|
368
|
+
z.object({
|
|
369
|
+
userComment: z.object({
|
|
370
|
+
text: z.string().optional(),
|
|
371
|
+
lastModified: z.object({
|
|
372
|
+
seconds: z.string().optional(),
|
|
373
|
+
nanos: z.number().optional()
|
|
374
|
+
}).optional(),
|
|
375
|
+
starRating: z.number().int().optional(),
|
|
376
|
+
reviewerLanguage: z.string().optional(),
|
|
377
|
+
device: z.string().optional(),
|
|
378
|
+
androidOsVersion: z.number().int().optional(),
|
|
379
|
+
appVersionCode: z.number().int().optional(),
|
|
380
|
+
appVersionName: z.string().optional()
|
|
381
|
+
}).optional()
|
|
382
|
+
})
|
|
383
|
+
).optional()
|
|
303
384
|
})
|
|
304
|
-
).optional()
|
|
385
|
+
).optional(),
|
|
386
|
+
tokenPagination: z.object({ nextPageToken: z.string().optional() }).optional()
|
|
305
387
|
});
|
|
306
388
|
var googlePlayConsoleResources = defineResources({
|
|
307
389
|
apps: {
|
|
308
390
|
shape: "entity",
|
|
309
391
|
filterable: [],
|
|
310
|
-
description: "Android app the connector is syncing. One entity per configured packageName.",
|
|
311
|
-
endpoint: "GET /androidpublisher/v3/applications/{packageName}/listings",
|
|
392
|
+
description: "Android app the connector is syncing. One entity per configured packageName, derived from the connector config; the Play Store listing title is only reachable through an Android Publisher edit and is not fetched.",
|
|
312
393
|
fields: [
|
|
313
394
|
{
|
|
314
395
|
name: "package_name",
|
|
315
396
|
description: "Reverse-DNS application id (e.g. com.example.app)."
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
name: "title",
|
|
319
|
-
description: "Play Store listing title in the default language. Empty if the listing has not been fetched yet."
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
name: "default_language",
|
|
323
|
-
description: "Default language code (BCP-47) configured for the Play Store listings."
|
|
324
397
|
}
|
|
325
|
-
]
|
|
326
|
-
responses: { listings: publisherListingSchema }
|
|
398
|
+
]
|
|
327
399
|
},
|
|
328
400
|
gplay_crash_rate_by_day: {
|
|
329
401
|
shape: "metric",
|
|
@@ -332,7 +404,10 @@ var googlePlayConsoleResources = defineResources({
|
|
|
332
404
|
granularity: "day",
|
|
333
405
|
endpoint: "POST /v1beta1/apps/{packageName}/crashRateMetricSet:query",
|
|
334
406
|
dimensions: [
|
|
335
|
-
{
|
|
407
|
+
{
|
|
408
|
+
name: "date",
|
|
409
|
+
description: "Calendar day of the metric sample (America/Los_Angeles, the only timezone the Reporting API supports for daily aggregation)."
|
|
410
|
+
},
|
|
336
411
|
{
|
|
337
412
|
name: "package_name",
|
|
338
413
|
description: "Reverse-DNS application id this sample is reported against."
|
|
@@ -347,7 +422,10 @@ var googlePlayConsoleResources = defineResources({
|
|
|
347
422
|
granularity: "day",
|
|
348
423
|
endpoint: "POST /v1beta1/apps/{packageName}/anrRateMetricSet:query",
|
|
349
424
|
dimensions: [
|
|
350
|
-
{
|
|
425
|
+
{
|
|
426
|
+
name: "date",
|
|
427
|
+
description: "Calendar day of the metric sample (America/Los_Angeles, the only timezone the Reporting API supports for daily aggregation)."
|
|
428
|
+
},
|
|
351
429
|
{
|
|
352
430
|
name: "package_name",
|
|
353
431
|
description: "Reverse-DNS application id this sample is reported against."
|
|
@@ -355,35 +433,54 @@ var googlePlayConsoleResources = defineResources({
|
|
|
355
433
|
],
|
|
356
434
|
responses: { anr_rate: metricSetSchema() }
|
|
357
435
|
},
|
|
358
|
-
|
|
436
|
+
gplay_error_count_by_day: {
|
|
359
437
|
shape: "metric",
|
|
360
|
-
description: "Daily
|
|
361
|
-
unit: "
|
|
438
|
+
description: "Daily count of error reports (crashes + ANRs + handled errors) from the Play Developer Reporting API.",
|
|
439
|
+
unit: "reports",
|
|
362
440
|
granularity: "day",
|
|
363
|
-
endpoint: "POST /v1beta1/apps/{packageName}/
|
|
441
|
+
endpoint: "POST /v1beta1/apps/{packageName}/errorCountMetricSet:query",
|
|
364
442
|
dimensions: [
|
|
365
|
-
{
|
|
443
|
+
{
|
|
444
|
+
name: "date",
|
|
445
|
+
description: "Calendar day of the metric sample (America/Los_Angeles, the only timezone the Reporting API supports for daily aggregation)."
|
|
446
|
+
},
|
|
366
447
|
{
|
|
367
448
|
name: "package_name",
|
|
368
449
|
description: "Reverse-DNS application id this sample is reported against."
|
|
369
450
|
}
|
|
370
451
|
],
|
|
371
|
-
responses: {
|
|
452
|
+
responses: { errors: metricSetSchema() }
|
|
372
453
|
},
|
|
373
|
-
|
|
454
|
+
gplay_app_ratings: {
|
|
374
455
|
shape: "metric",
|
|
375
|
-
description: "
|
|
376
|
-
unit: "
|
|
377
|
-
|
|
378
|
-
|
|
456
|
+
description: "Rolling per-review star ratings sampled from the most-recent user reviews via the Android Publisher reviews API (default 200, configurable via reviewLimit). Each sample carries one review with its star rating (1-5) as the value.",
|
|
457
|
+
unit: "stars",
|
|
458
|
+
endpoint: "GET /androidpublisher/v3/applications/{packageName}/reviews",
|
|
459
|
+
notes: "Not the lifetime average shown on the Play Store. The reviews API only returns reviews from roughly the past week, so this is a rolling sample; average over a time window downstream for a smoothed rating.",
|
|
379
460
|
dimensions: [
|
|
380
|
-
{ name: "date", description: "Calendar day of the metric sample (UTC)." },
|
|
381
461
|
{
|
|
382
462
|
name: "package_name",
|
|
383
|
-
description: "Reverse-DNS application id this
|
|
463
|
+
description: "Reverse-DNS application id this review was filed against."
|
|
464
|
+
},
|
|
465
|
+
{ name: "review_id", description: "Unique identifier of the review." },
|
|
466
|
+
{
|
|
467
|
+
name: "reviewer_language",
|
|
468
|
+
description: "BCP-47 language code the review was written in."
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "device",
|
|
472
|
+
description: "Codename of the device the review was filed from."
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "app_version_name",
|
|
476
|
+
description: "App version name the reviewer was running."
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: "android_os_version",
|
|
480
|
+
description: "Android SDK version the reviewer was running."
|
|
384
481
|
}
|
|
385
482
|
],
|
|
386
|
-
responses: {
|
|
483
|
+
responses: { reviews: reviewsResponseSchema }
|
|
387
484
|
}
|
|
388
485
|
});
|
|
389
486
|
var id = "google-play-console";
|
|
@@ -396,7 +493,8 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
396
493
|
return new _GooglePlayConsoleConnector(
|
|
397
494
|
{
|
|
398
495
|
packageName: parsed.packageName,
|
|
399
|
-
lookbackDays: parsed.lookbackDays
|
|
496
|
+
lookbackDays: parsed.lookbackDays,
|
|
497
|
+
reviewLimit: parsed.reviewLimit
|
|
400
498
|
},
|
|
401
499
|
{
|
|
402
500
|
serviceAccountJson: parsed.serviceAccountJson
|
|
@@ -434,18 +532,6 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
434
532
|
this.cachedToken = await this.fetchOAuthToken(url, body, signal);
|
|
435
533
|
return this.cachedToken.token;
|
|
436
534
|
}
|
|
437
|
-
async fetchListings(accessToken, signal) {
|
|
438
|
-
const url = `${PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(this.settings.packageName)}/listings`;
|
|
439
|
-
const res = await this.get(url, {
|
|
440
|
-
resource: "listings",
|
|
441
|
-
headers: {
|
|
442
|
-
Authorization: `Bearer ${accessToken}`,
|
|
443
|
-
"User-Agent": connectorUserAgent("google-play-console")
|
|
444
|
-
},
|
|
445
|
-
signal
|
|
446
|
-
});
|
|
447
|
-
return res.body;
|
|
448
|
-
}
|
|
449
535
|
async runMetricQuery(accessToken, cfg, dateRange, pageToken, signal) {
|
|
450
536
|
const url = `${REPORTING_BASE}/v1beta1/apps/${encodeURIComponent(this.settings.packageName)}/${cfg.metricSet}:query`;
|
|
451
537
|
const [sy, sm, sd] = dateRange.startDate.split("-").map(Number);
|
|
@@ -458,13 +544,13 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
458
544
|
year: sy,
|
|
459
545
|
month: sm,
|
|
460
546
|
day: sd,
|
|
461
|
-
timeZone: { id:
|
|
547
|
+
timeZone: { id: DAILY_TIME_ZONE }
|
|
462
548
|
},
|
|
463
549
|
endTime: {
|
|
464
550
|
year: ey,
|
|
465
551
|
month: em,
|
|
466
552
|
day: ed,
|
|
467
|
-
timeZone: { id:
|
|
553
|
+
timeZone: { id: DAILY_TIME_ZONE }
|
|
468
554
|
}
|
|
469
555
|
}
|
|
470
556
|
};
|
|
@@ -472,7 +558,7 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
472
558
|
body["pageToken"] = pageToken;
|
|
473
559
|
}
|
|
474
560
|
const res = await this.post(url, {
|
|
475
|
-
resource: cfg.
|
|
561
|
+
resource: cfg.responseTag,
|
|
476
562
|
headers: {
|
|
477
563
|
Authorization: `Bearer ${accessToken}`,
|
|
478
564
|
"Content-Type": "application/json",
|
|
@@ -483,33 +569,58 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
483
569
|
});
|
|
484
570
|
return res.body;
|
|
485
571
|
}
|
|
486
|
-
async syncApps(
|
|
487
|
-
let title = "";
|
|
488
|
-
let defaultLanguage = "";
|
|
489
|
-
try {
|
|
490
|
-
const listings = await this.fetchListings(accessToken, signal);
|
|
491
|
-
defaultLanguage = listings.defaultLanguage ?? "";
|
|
492
|
-
const list = listings.listings ?? [];
|
|
493
|
-
const def = list.find((l) => l.language === defaultLanguage) ?? list[0];
|
|
494
|
-
title = def?.title ?? "";
|
|
495
|
-
} catch (err) {
|
|
496
|
-
this.logger.warn(
|
|
497
|
-
"Failed to fetch Play Console listings; emitting app entity with empty title",
|
|
498
|
-
{ error: err.message }
|
|
499
|
-
);
|
|
500
|
-
}
|
|
572
|
+
async syncApps(storage) {
|
|
501
573
|
const entity = {
|
|
502
574
|
type: "apps",
|
|
503
575
|
id: this.settings.packageName,
|
|
504
576
|
attributes: {
|
|
505
|
-
package_name: this.settings.packageName
|
|
506
|
-
title,
|
|
507
|
-
default_language: defaultLanguage
|
|
577
|
+
package_name: this.settings.packageName
|
|
508
578
|
},
|
|
509
579
|
updated_at: Date.now()
|
|
510
580
|
};
|
|
511
581
|
await storage.entities([entity], { types: ["apps"] });
|
|
512
582
|
}
|
|
583
|
+
async fetchReviews(accessToken, signal) {
|
|
584
|
+
const reviews = [];
|
|
585
|
+
const base = `${PUBLISHER_BASE}/androidpublisher/v3/applications/${encodeURIComponent(this.settings.packageName)}/reviews`;
|
|
586
|
+
let token = void 0;
|
|
587
|
+
for (let page = 0; page < MAX_REVIEW_PAGES; page++) {
|
|
588
|
+
signal?.throwIfAborted();
|
|
589
|
+
const params = new URLSearchParams({
|
|
590
|
+
maxResults: String(REVIEWS_PAGE_SIZE)
|
|
591
|
+
});
|
|
592
|
+
if (token) {
|
|
593
|
+
params.set("token", token);
|
|
594
|
+
}
|
|
595
|
+
const res = await this.get(
|
|
596
|
+
`${base}?${params.toString()}`,
|
|
597
|
+
{
|
|
598
|
+
resource: "reviews",
|
|
599
|
+
headers: {
|
|
600
|
+
Authorization: `Bearer ${accessToken}`,
|
|
601
|
+
"User-Agent": connectorUserAgent("google-play-console")
|
|
602
|
+
},
|
|
603
|
+
signal
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
reviews.push(...res.body.reviews ?? []);
|
|
607
|
+
token = res.body.tokenPagination?.nextPageToken;
|
|
608
|
+
if (!token) {
|
|
609
|
+
return reviews;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
this.logger.warn(
|
|
613
|
+
`Stopped paginating Play Console reviews after ${MAX_REVIEW_PAGES} pages; the most-recent reviews are still ranked first but older reviews may be omitted`,
|
|
614
|
+
{ packageName: this.settings.packageName }
|
|
615
|
+
);
|
|
616
|
+
return reviews;
|
|
617
|
+
}
|
|
618
|
+
async syncReviews(accessToken, storage, signal) {
|
|
619
|
+
const limit = this.settings.reviewLimit ?? DEFAULT_REVIEW_LIMIT;
|
|
620
|
+
const reviews = await this.fetchReviews(accessToken, signal);
|
|
621
|
+
const samples = reviews.map((review) => reviewToRatingSample(review, this.settings.packageName)).filter((s) => s !== null).sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
622
|
+
await storage.metrics(samples, { names: [GPLAY_APP_RATINGS_METRIC] });
|
|
623
|
+
}
|
|
513
624
|
async drainMetricPhase(accessToken, cfg, dateRange, signal) {
|
|
514
625
|
const rows = [];
|
|
515
626
|
let pageToken = void 0;
|
|
@@ -542,17 +653,26 @@ var GooglePlayConsoleConnector = class _GooglePlayConsoleConnector extends BaseC
|
|
|
542
653
|
}
|
|
543
654
|
return accessToken;
|
|
544
655
|
};
|
|
545
|
-
const
|
|
656
|
+
const phases = selectActivePhases(
|
|
657
|
+
(resource) => RESOURCE_TO_PHASE[resource] ?? "apps",
|
|
658
|
+
PHASE_ORDER,
|
|
659
|
+
options.resources ? [...options.resources] : void 0
|
|
660
|
+
);
|
|
661
|
+
const resumeIdx = cursor ? phases.indexOf(cursor.phase) : -1;
|
|
546
662
|
const startIdx = resumeIdx >= 0 ? resumeIdx : 0;
|
|
547
|
-
for (let i = startIdx; i <
|
|
548
|
-
const phase =
|
|
663
|
+
for (let i = startIdx; i < phases.length; i++) {
|
|
664
|
+
const phase = phases[i];
|
|
549
665
|
if (signal?.aborted) {
|
|
550
666
|
return { done: false, cursor: { phase, dateRange } };
|
|
551
667
|
}
|
|
552
668
|
try {
|
|
553
669
|
if (phase === "apps") {
|
|
670
|
+
await this.syncApps(storage);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (phase === "reviews") {
|
|
554
674
|
const token2 = await getToken(signal);
|
|
555
|
-
await this.
|
|
675
|
+
await this.syncReviews(token2, storage, signal);
|
|
556
676
|
continue;
|
|
557
677
|
}
|
|
558
678
|
const cfg = METRIC_PHASE_CONFIGS[phase];
|