@hot-updater/console 1.0.0-rc.9 → 1.0.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/.output/nitro.json +1 -1
- package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
- package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
- package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
- package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
- package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
- package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
- package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
- package/.output/public/assets/api-DCM0wr6O.js +1 -0
- package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
- package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
- package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
- package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
- package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
- package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
- package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
- package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
- package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
- package/.output/public/assets/empty-CuT2UhFp.js +1 -0
- package/.output/public/assets/field-BBDpjIqu.js +1 -0
- package/.output/public/assets/index-6o51GkFe.js +11 -0
- package/.output/public/assets/input-group-ja_30qaF.js +1 -0
- package/.output/public/assets/insights-CmraDzQh.js +1 -0
- package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
- package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
- package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
- package/.output/public/assets/insights-vNRCrfnS.js +42 -0
- package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
- package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
- package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
- package/.output/public/assets/installations-BazSJXE0.js +2 -0
- package/.output/public/assets/installations-ClHTqdAO.js +1 -0
- package/.output/public/assets/installations-DpKsBuHi.js +1 -0
- package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
- package/.output/public/assets/list-CgrYiadM.js +1 -0
- package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
- package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
- package/.output/public/assets/remote-config-Br56BODb.js +6 -0
- package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
- package/.output/public/assets/routes-B88Erkpk.js +2 -0
- package/.output/public/assets/routes-CfzsAleC.js +1 -0
- package/.output/public/assets/search-DaTDPW25.js +1 -0
- package/.output/public/assets/select-BZZBXMkr.js +1 -0
- package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
- package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
- package/.output/public/assets/styles-DTGEAJFv.css +2 -0
- package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
- package/.output/public/assets/table-Gm4qRWwE.js +1 -0
- package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
- package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
- package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
- package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
- package/.output/server/_libs/lucide-react.mjs +221 -9
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +1 -1
- package/.output/server/_libs/recharts+[...].mjs +483 -901
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__history.mjs +68 -41
- package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
- package/.output/server/_libs/tanstack__react-query.mjs +448 -21
- package/.output/server/_libs/verkit.mjs +102 -1
- package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
- package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
- package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
- package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
- package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
- package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
- package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
- package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
- package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
- package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
- package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
- package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
- package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
- package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
- package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
- package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
- package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
- package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
- package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
- package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
- package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
- package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
- package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
- package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
- package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
- package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
- package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
- package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
- package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
- package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
- package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
- package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
- package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
- package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
- package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
- package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
- package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
- package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
- package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
- package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
- package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
- package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
- package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
- package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
- package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
- package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
- package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
- package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
- package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
- package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
- package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
- package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
- package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
- package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
- package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
- package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
- package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
- package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
- package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
- package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
- package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
- package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
- package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
- package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.mjs} +107 -47
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
- package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
- package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
- package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
- package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
- package/.output/server/index.mjs +309 -211
- package/LICENSE +13 -14
- package/README.md +80 -71
- package/lib/index.d.mts +23 -11
- package/lib/vite-internal.mjs +13 -3
- package/lib/vite.d.mts +2 -0
- package/package.json +14 -14
- package/src/components/AppSidebar.tsx +69 -74
- package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +103 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +8 -5
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
- package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
- package/src/components/features/insights/AppUsage.tsx +41 -14
- package/src/components/features/insights/BuiltinBundle.tsx +36 -0
- package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +390 -26
- package/src/components/features/insights/EventHistoryCard.tsx +111 -25
- package/src/components/features/insights/EventHistoryList.tsx +1 -1
- package/src/components/features/insights/FailureReports.tsx +407 -0
- package/src/components/features/insights/InsightsControls.tsx +240 -142
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsOverview.tsx +601 -403
- package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
- package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
- package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +308 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
- package/src/components/features/insights/RollbackButton.tsx +118 -0
- package/src/components/features/insights/UpdateFailures.tsx +382 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
- package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
- package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
- package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
- package/src/components/features/remote-config/ParametersCard.tsx +254 -0
- package/src/components/features/remote-config/PreviewCard.tsx +284 -0
- package/src/components/features/remote-config/PublishDialog.tsx +325 -0
- package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
- package/src/components/features/remote-config/RuleSummary.tsx +21 -0
- package/src/components/features/remote-config/ValueInput.tsx +151 -0
- package/src/components/features/remote-config/VersionsCard.tsx +384 -0
- package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
- package/src/components/features/remote-config/useDateTimeText.ts +16 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/index.ts +26 -19
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +125 -288
- package/src/lib/api.ts +15 -134
- package/src/lib/bundle-activity.ts +24 -4
- package/src/lib/console-access.ts +20 -0
- package/src/lib/console-features-api.ts +68 -0
- package/src/lib/console-features-rpc.ts +13 -0
- package/src/lib/console-features.ts +98 -0
- package/src/lib/insights-api.ts +56 -2
- package/src/lib/insights-errors.ts +64 -0
- package/src/lib/insights-failures-rpc.ts +36 -0
- package/src/lib/insights-failures.ts +130 -0
- package/src/lib/insights-recovery-rpc.ts +13 -17
- package/src/lib/insights-recovery.ts +12 -36
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +50 -4
- package/src/lib/insights-usage-rpc.ts +15 -9
- package/src/lib/insights-usage.ts +41 -3
- package/src/lib/insights-view.ts +34 -5
- package/src/lib/release-adoption-api.ts +214 -0
- package/src/lib/release-adoption-rpc.ts +49 -0
- package/src/lib/release-adoption.ts +164 -0
- package/src/lib/remote-config-api.ts +119 -0
- package/src/lib/remote-config-draft.ts +666 -0
- package/src/lib/remote-config-rpc.ts +196 -0
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +27 -28
- package/src/lib/server/config.server.ts +21 -76
- package/src/lib/server/console-feature-apis.ts +70 -0
- package/src/lib/server/console-runtime.server.ts +82 -38
- package/src/lib/server/downloadBundle.ts +14 -15
- package/src/lib/server/failureReports.ts +50 -0
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsUsage.ts +52 -113
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseAdoption.ts +151 -0
- package/src/lib/server/releaseReachability.ts +8 -8
- package/src/lib/server/remoteConfig.ts +75 -0
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +66 -0
- package/src/lib/use-dialog-target.ts +27 -0
- package/src/routeTree.gen.ts +44 -44
- package/src/router.tsx +3 -2
- package/src/routes/-installations-search.ts +32 -8
- package/src/routes/-releases-search.ts +76 -29
- package/src/routes/__root.tsx +7 -1
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +210 -152
- package/src/routes/insights.tsx +91 -23
- package/src/routes/insights_.distribution.tsx +50 -9
- package/src/routes/installations.tsx +97 -40
- package/src/routes/remote-config.tsx +42 -0
- package/src/styles.css +9 -0
- package/src/vite-internal.ts +15 -3
- package/src/vite.ts +2 -0
- package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
- package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
- package/.output/public/assets/api-CdUyUqzR.js +0 -42
- package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
- package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
- package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
- package/.output/public/assets/card-CYHs5FZv.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
- package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
- package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
- package/.output/public/assets/dist-BR4GzeEu.js +0 -1
- package/.output/public/assets/field-B3CYDpjg.js +0 -1
- package/.output/public/assets/index-Cb_D1D6N.js +0 -10
- package/.output/public/assets/info-C3y9HNoU.js +0 -1
- package/.output/public/assets/input-group-nLZylhWF.js +0 -1
- package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
- package/.output/public/assets/insights-DS3iO7Up.js +0 -1
- package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
- package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
- package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
- package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
- package/.output/public/assets/installations-CkFW4fV9.js +0 -2
- package/.output/public/assets/installations-DxTaIcHD.js +0 -1
- package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
- package/.output/public/assets/list-Dqt53fWZ.js +0 -1
- package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
- package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
- package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
- package/.output/public/assets/routes-Cgtvspjg.js +0 -2
- package/.output/public/assets/routes-O2r2UUN8.js +0 -1
- package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
- package/.output/public/assets/signing-BgspC-xP.js +0 -1
- package/.output/public/assets/styles-DnrWuBwd.css +0 -2
- package/.output/public/assets/table-BuWQgxJJ.js +0 -1
- package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
- package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
- package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
- package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
- package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
- package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
- package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
- package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
- package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
- package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
- package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
- package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
- package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
- package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
- package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
- package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
- package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
- package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
- package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
- package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
- package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
- package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
- package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
- package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
- package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
- package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
- package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
- package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
- package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
- package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
- package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
- package/src/components/features/signing/BundleSigningPage.tsx +0 -139
- package/src/lib/bundle-signing-api.ts +0 -14
- package/src/lib/bundle-signing-rpc.ts +0 -16
- package/src/lib/server/bundle-signing.server.ts +0 -23
- package/src/lib/server/deleteBundle.ts +0 -145
- package/src/lib/server/insightsHistory.ts +0 -52
- package/src/lib/server/insightsRecovery.ts +0 -187
- package/src/routes/signing.tsx +0 -37
|
@@ -0,0 +1,2086 @@
|
|
|
1
|
+
import { i as isDatabaseJsonObject, t as DatabaseAdapterInputError } from "./databaseJsonValue-Csbuzc9Q.mjs";
|
|
2
|
+
import { C as isUUIDv7, T as normalizeCohortValue, g as getNumericCohortValue, h as getNumericCohortRolloutPosition, w as isValidCohort } from "./dist-CbQbBIw-.mjs";
|
|
3
|
+
import { t as __exportAll } from "./rolldown-runtime-rSIU-vHC.mjs";
|
|
4
|
+
import { t as createUUIDv7 } from "./uuidv7-C88s9jWF.mjs";
|
|
5
|
+
import { c as satisfies, i as defineTable, r as defineAggregate, t as coerce } from "./schema-BiAVyEj2.mjs";
|
|
6
|
+
import { t as isValidRange } from "../_libs/verkit.mjs";
|
|
7
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/plugins-BaD1slHx.js
|
|
8
|
+
var semverSatisfies = (targetAppVersion, currentVersion) => {
|
|
9
|
+
const currentCoerce = coerce(currentVersion);
|
|
10
|
+
if (!currentCoerce) return false;
|
|
11
|
+
return satisfies(currentCoerce, targetAppVersion);
|
|
12
|
+
};
|
|
13
|
+
defineTable({
|
|
14
|
+
id: {
|
|
15
|
+
type: "string",
|
|
16
|
+
maxLength: 255
|
|
17
|
+
},
|
|
18
|
+
hash: {
|
|
19
|
+
type: "string",
|
|
20
|
+
maxLength: 64,
|
|
21
|
+
unique: true
|
|
22
|
+
},
|
|
23
|
+
name: {
|
|
24
|
+
type: "string",
|
|
25
|
+
maxLength: 64
|
|
26
|
+
},
|
|
27
|
+
prefix: {
|
|
28
|
+
type: "string",
|
|
29
|
+
maxLength: 16
|
|
30
|
+
},
|
|
31
|
+
role: {
|
|
32
|
+
type: "string",
|
|
33
|
+
maxLength: 16
|
|
34
|
+
},
|
|
35
|
+
created_at_ms: { type: "integer" },
|
|
36
|
+
revoked_at_ms: {
|
|
37
|
+
type: "integer",
|
|
38
|
+
required: false
|
|
39
|
+
}
|
|
40
|
+
}, {
|
|
41
|
+
key: ["id"],
|
|
42
|
+
indexes: { byCreated: {
|
|
43
|
+
eq: [],
|
|
44
|
+
sort: ["created_at_ms"]
|
|
45
|
+
} }
|
|
46
|
+
});
|
|
47
|
+
var DAY_MS$1 = 864e5;
|
|
48
|
+
var DEFAULT_INSIGHTS_RETENTION = {
|
|
49
|
+
rawDays: 90,
|
|
50
|
+
dailyDays: 400
|
|
51
|
+
};
|
|
52
|
+
var bucketRetention = (days) => ({
|
|
53
|
+
field: "bucket_start_ms",
|
|
54
|
+
days
|
|
55
|
+
});
|
|
56
|
+
/** Every kind of row the table keeps belongs in installation history. */
|
|
57
|
+
var MOVEMENTS = /* @__PURE__ */ new Set([
|
|
58
|
+
"UPDATE_DOWNLOADED",
|
|
59
|
+
"UPDATE_APPLIED",
|
|
60
|
+
"RECOVERED",
|
|
61
|
+
"UPDATE_FAILED",
|
|
62
|
+
"UNCHANGED"
|
|
63
|
+
]);
|
|
64
|
+
/** A failed update check: it names no target bundle, so no bundle list shows it. */
|
|
65
|
+
var isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
|
|
66
|
+
var metadataOf = (row) => row.metadata ?? {};
|
|
67
|
+
/** A download or apply that arrived after its target ran: it counts nothing. */
|
|
68
|
+
var isLateEvent = (row) => metadataOf(row).late === true;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a stored row launched its installation into a bundle release: an
|
|
71
|
+
* apply, or a kept UNCHANGED row that moved the installation to another
|
|
72
|
+
* bundle of a release, as when the apply report never arrived or a rollback
|
|
73
|
+
* to the built-in bundle took it there. A first report, a release change of
|
|
74
|
+
* the bundle already running, and a late row launch nothing.
|
|
75
|
+
*/
|
|
76
|
+
var isLaunch = (row) => {
|
|
77
|
+
if (isLateEvent(row)) return false;
|
|
78
|
+
if (row.type === "UPDATE_APPLIED") return true;
|
|
79
|
+
if (row.type !== "UNCHANGED") return false;
|
|
80
|
+
const kinds = metadataOf(row).change?.kinds ?? [];
|
|
81
|
+
return kinds.includes("bundle") && !kinds.includes("first_seen") && row.to_release_id !== null;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The bundle a stored row's bundle filter matches: `from:<bundle>` for a
|
|
85
|
+
* recovery; `to:<bundle>` for a download, an apply, and an update failure;
|
|
86
|
+
* `on:<bundle>` for an UNCHANGED row that launched a bundle (`isLaunch`), a
|
|
87
|
+
* key no other row uses. None for a failed check, which targets no bundle, a
|
|
88
|
+
* late row, or any other UNCHANGED row.
|
|
89
|
+
*/
|
|
90
|
+
var bundleRefsOf = (row) => {
|
|
91
|
+
if (isFailedCheck(row) || isLateEvent(row)) return [];
|
|
92
|
+
if (row.type === "RECOVERED") return [`from:${row.from_bundle_id}`];
|
|
93
|
+
if (row.type === "UNCHANGED") return isLaunch(row) ? [`on:${row.to_bundle_id}`] : [];
|
|
94
|
+
return [`to:${row.to_bundle_id}`];
|
|
95
|
+
};
|
|
96
|
+
/** The key a bundle filter reads, as `bundleRefsOf` writes it. */
|
|
97
|
+
var bundleRefOfFilter = (filter) => filter.type === "RECOVERED" ? `from:${filter.fromBundleId}` : filter.type === "UNCHANGED" ? `on:${filter.toBundleId}` : `to:${filter.toBundleId}`;
|
|
98
|
+
/**
|
|
99
|
+
* Downloads, applies, recoveries, update failures, and the UNCHANGED
|
|
100
|
+
* reports that changed what their installation runs, whole. An UNCHANGED
|
|
101
|
+
* report that changes nothing counts as a launch and moves its
|
|
102
|
+
* installation's head, but no row keeps it.
|
|
103
|
+
*/
|
|
104
|
+
var bundleEvents = (days) => defineTable({
|
|
105
|
+
id: {
|
|
106
|
+
type: "string",
|
|
107
|
+
maxLength: 36
|
|
108
|
+
},
|
|
109
|
+
type: {
|
|
110
|
+
type: "string",
|
|
111
|
+
maxLength: 32
|
|
112
|
+
},
|
|
113
|
+
install_id: {
|
|
114
|
+
type: "string",
|
|
115
|
+
maxLength: 255
|
|
116
|
+
},
|
|
117
|
+
user_id: {
|
|
118
|
+
type: "string",
|
|
119
|
+
maxLength: 255,
|
|
120
|
+
required: false
|
|
121
|
+
},
|
|
122
|
+
from_release_id: {
|
|
123
|
+
type: "string",
|
|
124
|
+
maxLength: 36,
|
|
125
|
+
required: false
|
|
126
|
+
},
|
|
127
|
+
from_bundle_id: {
|
|
128
|
+
type: "string",
|
|
129
|
+
maxLength: 36,
|
|
130
|
+
required: false
|
|
131
|
+
},
|
|
132
|
+
to_release_id: {
|
|
133
|
+
type: "string",
|
|
134
|
+
maxLength: 36,
|
|
135
|
+
required: false
|
|
136
|
+
},
|
|
137
|
+
to_bundle_id: {
|
|
138
|
+
type: "string",
|
|
139
|
+
maxLength: 36
|
|
140
|
+
},
|
|
141
|
+
platform: {
|
|
142
|
+
type: "string",
|
|
143
|
+
maxLength: 16
|
|
144
|
+
},
|
|
145
|
+
app_version: { type: "string" },
|
|
146
|
+
channel: { type: "string" },
|
|
147
|
+
metadata: { type: "json" },
|
|
148
|
+
received_at_ms: { type: "integer" }
|
|
149
|
+
}, {
|
|
150
|
+
key: ["id"],
|
|
151
|
+
derived: {
|
|
152
|
+
day: {
|
|
153
|
+
type: "integer",
|
|
154
|
+
compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS$1
|
|
155
|
+
},
|
|
156
|
+
movement_install_id: {
|
|
157
|
+
type: "string",
|
|
158
|
+
compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
|
|
159
|
+
},
|
|
160
|
+
bundle_ref: {
|
|
161
|
+
type: "string",
|
|
162
|
+
multi: true,
|
|
163
|
+
compute: (row) => bundleRefsOf(row)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
indexes: {
|
|
167
|
+
movementsByInstall: {
|
|
168
|
+
eq: ["movement_install_id"],
|
|
169
|
+
sort: ["received_at_ms"]
|
|
170
|
+
},
|
|
171
|
+
byBundle: {
|
|
172
|
+
eq: [
|
|
173
|
+
"platform",
|
|
174
|
+
"channel",
|
|
175
|
+
"type",
|
|
176
|
+
"bundle_ref",
|
|
177
|
+
"day"
|
|
178
|
+
],
|
|
179
|
+
sort: ["received_at_ms"]
|
|
180
|
+
},
|
|
181
|
+
byDay: {
|
|
182
|
+
eq: ["day"],
|
|
183
|
+
sort: ["received_at_ms"]
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
retention: {
|
|
187
|
+
field: "day",
|
|
188
|
+
days
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Each installation's latest event, whole, so reading it is one point read.
|
|
193
|
+
* Its gauges count it in the UTC day of that event. An update failure moves
|
|
194
|
+
* nothing, so no head holds one.
|
|
195
|
+
*/
|
|
196
|
+
var bundleEventHeads = (days) => defineTable({
|
|
197
|
+
install_id: {
|
|
198
|
+
type: "string",
|
|
199
|
+
maxLength: 255
|
|
200
|
+
},
|
|
201
|
+
id: {
|
|
202
|
+
type: "string",
|
|
203
|
+
maxLength: 36
|
|
204
|
+
},
|
|
205
|
+
type: {
|
|
206
|
+
type: "string",
|
|
207
|
+
maxLength: 32
|
|
208
|
+
},
|
|
209
|
+
user_id: {
|
|
210
|
+
type: "string",
|
|
211
|
+
maxLength: 255,
|
|
212
|
+
required: false
|
|
213
|
+
},
|
|
214
|
+
from_release_id: {
|
|
215
|
+
type: "string",
|
|
216
|
+
maxLength: 36,
|
|
217
|
+
required: false
|
|
218
|
+
},
|
|
219
|
+
from_bundle_id: {
|
|
220
|
+
type: "string",
|
|
221
|
+
maxLength: 36,
|
|
222
|
+
required: false
|
|
223
|
+
},
|
|
224
|
+
to_release_id: {
|
|
225
|
+
type: "string",
|
|
226
|
+
maxLength: 36,
|
|
227
|
+
required: false
|
|
228
|
+
},
|
|
229
|
+
to_bundle_id: {
|
|
230
|
+
type: "string",
|
|
231
|
+
maxLength: 36
|
|
232
|
+
},
|
|
233
|
+
platform: {
|
|
234
|
+
type: "string",
|
|
235
|
+
maxLength: 16
|
|
236
|
+
},
|
|
237
|
+
app_version: { type: "string" },
|
|
238
|
+
channel: { type: "string" },
|
|
239
|
+
metadata: { type: "json" },
|
|
240
|
+
received_at_ms: { type: "integer" }
|
|
241
|
+
}, {
|
|
242
|
+
key: ["install_id"],
|
|
243
|
+
indexes: { byUser: {
|
|
244
|
+
eq: ["user_id"],
|
|
245
|
+
sort: ["install_id"]
|
|
246
|
+
} },
|
|
247
|
+
retention: {
|
|
248
|
+
field: "received_at_ms",
|
|
249
|
+
days
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
/**
|
|
253
|
+
* Shards are keyed by install id. Counters are blind increments and never
|
|
254
|
+
* conflict, so they keep 8. Gauges and sketches are read, merged, and written
|
|
255
|
+
* back. B3's rollout gate on PostgreSQL retried 2.4% of transactions at 8
|
|
256
|
+
* shards and 1.3% at 16. DynamoDB's transactions take longer, so D8's gate
|
|
257
|
+
* on DynamoDB Local retried 2–24% at 16, almost all on gauge rows, and 1–5%
|
|
258
|
+
* with gauges at 32. Sketches stay at 16: each shard row carries 2 KB of
|
|
259
|
+
* registers that every read merges.
|
|
260
|
+
*
|
|
261
|
+
* Every Insights aggregate is `batched`: on DynamoDB and Firestore, which
|
|
262
|
+
* bill each write, an event's changes apply after it commits, merged with
|
|
263
|
+
* other events' into one write per row, on one shard per writer.
|
|
264
|
+
*/
|
|
265
|
+
var COUNTER_SHARDS = 8;
|
|
266
|
+
var GAUGE_SHARDS = 32;
|
|
267
|
+
var SKETCH_SHARDS = 16;
|
|
268
|
+
/** Rows keyed by a hashed identity (scope and period) and a bucket. */
|
|
269
|
+
var identityFields = {
|
|
270
|
+
identity: {
|
|
271
|
+
type: "string",
|
|
272
|
+
maxLength: 32
|
|
273
|
+
},
|
|
274
|
+
bucket_start_ms: { type: "integer" }
|
|
275
|
+
};
|
|
276
|
+
var window = {
|
|
277
|
+
eq: ["identity"],
|
|
278
|
+
sort: ["bucket_start_ms"]
|
|
279
|
+
};
|
|
280
|
+
/** What a release or channel row counts in every period kind. */
|
|
281
|
+
var WINDOW_COUNTERS = [
|
|
282
|
+
"downloads",
|
|
283
|
+
"failed_launches",
|
|
284
|
+
"patch_downloads",
|
|
285
|
+
"patch_fallbacks"
|
|
286
|
+
];
|
|
287
|
+
/**
|
|
288
|
+
* Hourly or daily counters: a release or channel row counts downloads,
|
|
289
|
+
* launches that crashed and recovered (`failed_launches`), and downloads a
|
|
290
|
+
* patch delivered and those that fell back from one.
|
|
291
|
+
*/
|
|
292
|
+
var counters = (retention) => defineAggregate(identityFields, {
|
|
293
|
+
key: ["identity", "bucket_start_ms"],
|
|
294
|
+
counters: WINDOW_COUNTERS,
|
|
295
|
+
shards: COUNTER_SHARDS,
|
|
296
|
+
batched: true,
|
|
297
|
+
indexes: { window },
|
|
298
|
+
retention
|
|
299
|
+
});
|
|
300
|
+
/**
|
|
301
|
+
* A release's counters since its first report, at bucket 0: the windowed
|
|
302
|
+
* counters, its applies, which the bundle list reads, and its failed updates
|
|
303
|
+
* (`failed_updates`), since no breakdown row outlives the raw period; windowed
|
|
304
|
+
* reads sum `insights_failures` instead.
|
|
305
|
+
*/
|
|
306
|
+
var lifetimeCounters = () => defineAggregate(identityFields, {
|
|
307
|
+
key: ["identity", "bucket_start_ms"],
|
|
308
|
+
counters: [
|
|
309
|
+
...WINDOW_COUNTERS,
|
|
310
|
+
"applies",
|
|
311
|
+
"failed_updates"
|
|
312
|
+
],
|
|
313
|
+
shards: COUNTER_SHARDS,
|
|
314
|
+
batched: true,
|
|
315
|
+
indexes: { window }
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* Unique-installation sketches of one period kind: a usage row's active
|
|
319
|
+
* installations (`activity_users`), and a `failure` or `check` row's
|
|
320
|
+
* installations whose update or update check failed (`failed_users`).
|
|
321
|
+
*/
|
|
322
|
+
var sketches = (retention) => defineAggregate(identityFields, {
|
|
323
|
+
key: ["identity", "bucket_start_ms"],
|
|
324
|
+
distinct: ["activity_users", "failed_users"],
|
|
325
|
+
shards: SKETCH_SHARDS,
|
|
326
|
+
batched: true,
|
|
327
|
+
indexes: { window },
|
|
328
|
+
retention
|
|
329
|
+
});
|
|
330
|
+
/** Installations whose update to a release failed, since its first failure: kept, at bucket 0. */
|
|
331
|
+
var lifetimeSketches = () => defineAggregate(identityFields, {
|
|
332
|
+
key: ["identity", "bucket_start_ms"],
|
|
333
|
+
distinct: ["failed_users"],
|
|
334
|
+
shards: SKETCH_SHARDS,
|
|
335
|
+
batched: true,
|
|
336
|
+
indexes: { window }
|
|
337
|
+
});
|
|
338
|
+
/**
|
|
339
|
+
* Update failures by what failed, a row an hour. `release_id` is the failed
|
|
340
|
+
* update's target release, or empty for a failed check or an unknown target.
|
|
341
|
+
* `stage` and `reason` say where and why it failed, and `detail` what else
|
|
342
|
+
* the client knew, as the JSON array `[resource, httpStatus, originCode,
|
|
343
|
+
* transport]`. Every index is in key order, so its counters stay blind
|
|
344
|
+
* increments.
|
|
345
|
+
*/
|
|
346
|
+
var insightsFailures = (retention) => defineAggregate({
|
|
347
|
+
platform: {
|
|
348
|
+
type: "string",
|
|
349
|
+
maxLength: 16
|
|
350
|
+
},
|
|
351
|
+
channel: { type: "string" },
|
|
352
|
+
bucket_start_ms: { type: "integer" },
|
|
353
|
+
release_id: {
|
|
354
|
+
type: "string",
|
|
355
|
+
maxLength: 36
|
|
356
|
+
},
|
|
357
|
+
stage: {
|
|
358
|
+
type: "string",
|
|
359
|
+
maxLength: 16
|
|
360
|
+
},
|
|
361
|
+
reason: {
|
|
362
|
+
type: "string",
|
|
363
|
+
maxLength: 64
|
|
364
|
+
},
|
|
365
|
+
detail: {
|
|
366
|
+
type: "string",
|
|
367
|
+
maxLength: 160
|
|
368
|
+
}
|
|
369
|
+
}, {
|
|
370
|
+
key: [
|
|
371
|
+
"platform",
|
|
372
|
+
"channel",
|
|
373
|
+
"bucket_start_ms",
|
|
374
|
+
"release_id",
|
|
375
|
+
"stage",
|
|
376
|
+
"reason",
|
|
377
|
+
"detail"
|
|
378
|
+
],
|
|
379
|
+
counters: ["events"],
|
|
380
|
+
shards: COUNTER_SHARDS,
|
|
381
|
+
batched: true,
|
|
382
|
+
indexes: { byScope: {
|
|
383
|
+
eq: ["platform", "channel"],
|
|
384
|
+
sort: ["bucket_start_ms"]
|
|
385
|
+
} },
|
|
386
|
+
retention
|
|
387
|
+
});
|
|
388
|
+
/** Installations counted in the UTC day of their latest event, so reads sum whole UTC days. */
|
|
389
|
+
var insightsDistribution = (retention) => defineAggregate({
|
|
390
|
+
channel: { type: "string" },
|
|
391
|
+
platform: {
|
|
392
|
+
type: "string",
|
|
393
|
+
maxLength: 16
|
|
394
|
+
},
|
|
395
|
+
app_version: { type: "string" },
|
|
396
|
+
release_id: {
|
|
397
|
+
type: "string",
|
|
398
|
+
maxLength: 36
|
|
399
|
+
},
|
|
400
|
+
bucket_start_ms: { type: "integer" }
|
|
401
|
+
}, {
|
|
402
|
+
key: [
|
|
403
|
+
"channel",
|
|
404
|
+
"platform",
|
|
405
|
+
"app_version",
|
|
406
|
+
"release_id",
|
|
407
|
+
"bucket_start_ms"
|
|
408
|
+
],
|
|
409
|
+
gauges: ["latest_installations"],
|
|
410
|
+
shards: GAUGE_SHARDS,
|
|
411
|
+
batched: true,
|
|
412
|
+
indexes: {
|
|
413
|
+
byScope: {
|
|
414
|
+
eq: ["channel", "platform"],
|
|
415
|
+
sort: ["bucket_start_ms"]
|
|
416
|
+
},
|
|
417
|
+
byVersion: {
|
|
418
|
+
eq: [
|
|
419
|
+
"channel",
|
|
420
|
+
"platform",
|
|
421
|
+
"app_version"
|
|
422
|
+
],
|
|
423
|
+
sort: ["bucket_start_ms"]
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
retention
|
|
427
|
+
});
|
|
428
|
+
/**
|
|
429
|
+
* The installations of `insights_distribution` that run their native build's
|
|
430
|
+
* built-in bundle, by its ID (`builtin_bundle_id`), within the same rows:
|
|
431
|
+
* reads move them out of the row of their release, or of no release.
|
|
432
|
+
*/
|
|
433
|
+
var insightsBuiltinDistribution = (retention) => defineAggregate({
|
|
434
|
+
channel: { type: "string" },
|
|
435
|
+
platform: {
|
|
436
|
+
type: "string",
|
|
437
|
+
maxLength: 16
|
|
438
|
+
},
|
|
439
|
+
app_version: { type: "string" },
|
|
440
|
+
release_id: {
|
|
441
|
+
type: "string",
|
|
442
|
+
maxLength: 36
|
|
443
|
+
},
|
|
444
|
+
builtin_bundle_id: {
|
|
445
|
+
type: "string",
|
|
446
|
+
maxLength: 36
|
|
447
|
+
},
|
|
448
|
+
bucket_start_ms: { type: "integer" }
|
|
449
|
+
}, {
|
|
450
|
+
key: [
|
|
451
|
+
"channel",
|
|
452
|
+
"platform",
|
|
453
|
+
"app_version",
|
|
454
|
+
"release_id",
|
|
455
|
+
"builtin_bundle_id",
|
|
456
|
+
"bucket_start_ms"
|
|
457
|
+
],
|
|
458
|
+
gauges: ["latest_installations"],
|
|
459
|
+
shards: GAUGE_SHARDS,
|
|
460
|
+
batched: true,
|
|
461
|
+
indexes: {
|
|
462
|
+
byScope: {
|
|
463
|
+
eq: ["channel", "platform"],
|
|
464
|
+
sort: ["bucket_start_ms"]
|
|
465
|
+
},
|
|
466
|
+
byVersion: {
|
|
467
|
+
eq: [
|
|
468
|
+
"channel",
|
|
469
|
+
"platform",
|
|
470
|
+
"app_version"
|
|
471
|
+
],
|
|
472
|
+
sort: ["bucket_start_ms"]
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
retention
|
|
476
|
+
});
|
|
477
|
+
/** Latest events by the bundle they came from or went to, bucketed like the distribution. */
|
|
478
|
+
var insightsLatestByBundle = (retention) => defineAggregate({
|
|
479
|
+
platform: {
|
|
480
|
+
type: "string",
|
|
481
|
+
maxLength: 16
|
|
482
|
+
},
|
|
483
|
+
channel: { type: "string" },
|
|
484
|
+
bundle_field: {
|
|
485
|
+
type: "string",
|
|
486
|
+
maxLength: 16
|
|
487
|
+
},
|
|
488
|
+
bundle_id: {
|
|
489
|
+
type: "string",
|
|
490
|
+
maxLength: 36
|
|
491
|
+
},
|
|
492
|
+
type: {
|
|
493
|
+
type: "string",
|
|
494
|
+
maxLength: 32
|
|
495
|
+
},
|
|
496
|
+
bucket_start_ms: { type: "integer" }
|
|
497
|
+
}, {
|
|
498
|
+
key: [
|
|
499
|
+
"platform",
|
|
500
|
+
"channel",
|
|
501
|
+
"bundle_field",
|
|
502
|
+
"bundle_id",
|
|
503
|
+
"type",
|
|
504
|
+
"bucket_start_ms"
|
|
505
|
+
],
|
|
506
|
+
gauges: ["installations"],
|
|
507
|
+
shards: GAUGE_SHARDS,
|
|
508
|
+
batched: true,
|
|
509
|
+
indexes: { byBundle: {
|
|
510
|
+
eq: [
|
|
511
|
+
"platform",
|
|
512
|
+
"channel",
|
|
513
|
+
"bundle_field",
|
|
514
|
+
"bundle_id",
|
|
515
|
+
"type"
|
|
516
|
+
],
|
|
517
|
+
sort: ["bucket_start_ms"]
|
|
518
|
+
} },
|
|
519
|
+
retention
|
|
520
|
+
});
|
|
521
|
+
/**
|
|
522
|
+
* Stored events by outcome and the bundle a bundle filter matches, a row an
|
|
523
|
+
* hour, which a failed check has none of; and every stored event, a row a UTC
|
|
524
|
+
* day (`DAILY_EVENTS`). Bundle counts sum the hourly rows, and event lists
|
|
525
|
+
* read either to skip days without events.
|
|
526
|
+
*/
|
|
527
|
+
var insightsOutcomes = (retention) => defineAggregate({
|
|
528
|
+
platform: {
|
|
529
|
+
type: "string",
|
|
530
|
+
maxLength: 16
|
|
531
|
+
},
|
|
532
|
+
channel: { type: "string" },
|
|
533
|
+
type: {
|
|
534
|
+
type: "string",
|
|
535
|
+
maxLength: 32
|
|
536
|
+
},
|
|
537
|
+
bundle_ref: {
|
|
538
|
+
type: "string",
|
|
539
|
+
maxLength: 41
|
|
540
|
+
},
|
|
541
|
+
bucket_start_ms: { type: "integer" }
|
|
542
|
+
}, {
|
|
543
|
+
key: [
|
|
544
|
+
"platform",
|
|
545
|
+
"channel",
|
|
546
|
+
"type",
|
|
547
|
+
"bundle_ref",
|
|
548
|
+
"bucket_start_ms"
|
|
549
|
+
],
|
|
550
|
+
counters: ["events"],
|
|
551
|
+
shards: COUNTER_SHARDS,
|
|
552
|
+
batched: true,
|
|
553
|
+
indexes: { byRef: {
|
|
554
|
+
eq: [
|
|
555
|
+
"platform",
|
|
556
|
+
"channel",
|
|
557
|
+
"type",
|
|
558
|
+
"bundle_ref"
|
|
559
|
+
],
|
|
560
|
+
sort: ["bucket_start_ms"]
|
|
561
|
+
} },
|
|
562
|
+
retention
|
|
563
|
+
});
|
|
564
|
+
/**
|
|
565
|
+
* Insights' tables, each expiring after its period. The periods are values
|
|
566
|
+
* the engine applies at runtime; the tables and indexes are the same for
|
|
567
|
+
* any periods.
|
|
568
|
+
*/
|
|
569
|
+
var createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTION) => {
|
|
570
|
+
const hourly = bucketRetention(rawDays);
|
|
571
|
+
const daily = bucketRetention(dailyDays);
|
|
572
|
+
return {
|
|
573
|
+
bundle_events: bundleEvents(rawDays),
|
|
574
|
+
bundle_event_heads: bundleEventHeads(dailyDays),
|
|
575
|
+
insights_overview: counters(hourly),
|
|
576
|
+
insights_sketches: sketches(hourly),
|
|
577
|
+
insights_overview_daily: counters(daily),
|
|
578
|
+
insights_sketches_daily: sketches(daily),
|
|
579
|
+
insights_overview_lifetime: lifetimeCounters(),
|
|
580
|
+
insights_sketches_lifetime: lifetimeSketches(),
|
|
581
|
+
insights_distribution: insightsDistribution(daily),
|
|
582
|
+
insights_builtin_distribution: insightsBuiltinDistribution(daily),
|
|
583
|
+
insights_latest_by_bundle: insightsLatestByBundle(daily),
|
|
584
|
+
insights_outcomes: insightsOutcomes(hourly),
|
|
585
|
+
insights_failures: insightsFailures(hourly)
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
createInsightsSchema();
|
|
589
|
+
var CHANGE_KINDS = /* @__PURE__ */ new Set([
|
|
590
|
+
"first_seen",
|
|
591
|
+
"bundle",
|
|
592
|
+
"release",
|
|
593
|
+
"app_version",
|
|
594
|
+
"channel",
|
|
595
|
+
"native_build"
|
|
596
|
+
]);
|
|
597
|
+
var isBundleEventChange = (value) => isDatabaseJsonObject(value) && Array.isArray(value.kinds) && value.kinds.length > 0 && value.kinds.every((kind) => CHANGE_KINDS.has(kind)) && (value.previous === null || isDatabaseJsonObject(value.previous) && typeof value.previous.bundle_id === "string" && (value.previous.release_id === null || typeof value.previous.release_id === "string") && typeof value.previous.app_version === "string" && typeof value.previous.channel === "string" && isOptional(value.previous, "min_bundle_id", isString));
|
|
598
|
+
var isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
|
|
599
|
+
var isString = (value) => typeof value === "string";
|
|
600
|
+
var isDatabaseHttpResponse = (value) => isDatabaseJsonObject(value) && (value.resource === "catalog" || value.resource === "artifact") && isString(value.path) && Number.isSafeInteger(value.status) && (value.body === null || isString(value.body)) && typeof value.body_truncated === "boolean" && Number.isSafeInteger(value.received_at_ms);
|
|
601
|
+
/** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
|
|
602
|
+
var isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "error_message", isString) && isOptional(value, "error_stack", isString);
|
|
603
|
+
var isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "min_bundle_id", isString) && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "http_response", isDatabaseHttpResponse) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "change", isBundleEventChange) && isOptional(value, "late", (flag) => flag === true) && isOptional(value, "implied_download", (flag) => flag === true);
|
|
604
|
+
var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
605
|
+
var isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
|
|
606
|
+
var isTextOrNull = (value) => value === null || typeof value === "string";
|
|
607
|
+
/** Each bundle event field and what it holds. */
|
|
608
|
+
var BUNDLE_EVENT_FIELDS = {
|
|
609
|
+
id: (value) => typeof value === "string",
|
|
610
|
+
type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
|
|
611
|
+
install_id: isIdentity$1,
|
|
612
|
+
user_id: (value) => value === null || isIdentity$1(value),
|
|
613
|
+
from_bundle_id: isTextOrNull,
|
|
614
|
+
from_release_id: isTextOrNull,
|
|
615
|
+
to_release_id: isTextOrNull,
|
|
616
|
+
to_bundle_id: (value) => typeof value === "string",
|
|
617
|
+
platform: (value) => value === "ios" || value === "android",
|
|
618
|
+
app_version: (value) => typeof value === "string",
|
|
619
|
+
channel: (value) => typeof value === "string",
|
|
620
|
+
metadata: isDatabaseBundleEventMetadata,
|
|
621
|
+
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
622
|
+
};
|
|
623
|
+
var hasMetadata = (row, key) => isRecord$3(row.metadata) && Object.hasOwn(row.metadata, key);
|
|
624
|
+
var updateStrategyOf = (row) => isRecord$3(row.metadata) ? row.metadata.update_strategy : void 0;
|
|
625
|
+
/**
|
|
626
|
+
* A movement, or a failed update, names the bundle it left and how the
|
|
627
|
+
* device updates, and a failed update says what failed; an unchanged report
|
|
628
|
+
* names neither.
|
|
629
|
+
*/
|
|
630
|
+
var hasEventInvariants = (row) => (row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED" || row.type === "UPDATE_FAILED") && typeof row.from_bundle_id === "string" && (updateStrategyOf(row) === "fingerprint" || updateStrategyOf(row) === "appVersion") && (row.type !== "UPDATE_FAILED" || isRecord$3(row.metadata) && isRecord$3(row.metadata.failure)) && !hasMetadata(row, "change") && (!hasMetadata(row, "late") || row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED") && (!hasMetadata(row, "implied_download") || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED") || row.type === "UNCHANGED" && !hasMetadata(row, "late") && row.from_bundle_id === null && updateStrategyOf(row) === null;
|
|
631
|
+
/**
|
|
632
|
+
* Throws unless `row` has exactly the bundle event fields, each of its type,
|
|
633
|
+
* and the invariants of its event type: `invalid-field` for an unknown field,
|
|
634
|
+
* `invalid-data` for anything else.
|
|
635
|
+
*/
|
|
636
|
+
var validateBundleEventFields = (row) => {
|
|
637
|
+
if (!isRecord$3(row)) throw new DatabaseAdapterInputError("invalid-data");
|
|
638
|
+
for (const field of Object.keys(row)) if (!Object.hasOwn(BUNDLE_EVENT_FIELDS, field)) throw new DatabaseAdapterInputError("invalid-field");
|
|
639
|
+
for (const [field, valid] of Object.entries(BUNDLE_EVENT_FIELDS)) if (!Object.hasOwn(row, field) || !valid(row[field])) throw new DatabaseAdapterInputError("invalid-data");
|
|
640
|
+
if (!hasEventInvariants(row)) throw new DatabaseAdapterInputError("invalid-data");
|
|
641
|
+
};
|
|
642
|
+
var encoder = new TextEncoder();
|
|
643
|
+
/** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
|
|
644
|
+
var compareInsightsText = (left, right) => {
|
|
645
|
+
const a = encoder.encode(left);
|
|
646
|
+
const b = encoder.encode(right);
|
|
647
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
648
|
+
return a.length - b.length;
|
|
649
|
+
};
|
|
650
|
+
var isWellFormedText = (value) => {
|
|
651
|
+
for (const character of value) {
|
|
652
|
+
const point = character.codePointAt(0);
|
|
653
|
+
if (point >= 55296 && point <= 57343) return false;
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
};
|
|
657
|
+
var isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
|
|
658
|
+
var isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
|
|
659
|
+
var isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
660
|
+
var isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
|
|
661
|
+
var hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
662
|
+
var hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
|
|
663
|
+
var isBundleFilter = (value, withKind = false) => {
|
|
664
|
+
if (!isRecord$3(value) || !hasScope(value)) return false;
|
|
665
|
+
const keys = [
|
|
666
|
+
"platform",
|
|
667
|
+
"channel",
|
|
668
|
+
"type",
|
|
669
|
+
...withKind ? ["kind"] : []
|
|
670
|
+
];
|
|
671
|
+
return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UPDATE_FAILED" || value.type === "UNCHANGED") && isText(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
|
|
672
|
+
};
|
|
673
|
+
var isEventFilter = (value) => {
|
|
674
|
+
if (!isRecord$3(value)) return false;
|
|
675
|
+
if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
|
|
676
|
+
if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
|
|
677
|
+
return value.kind === "bundle" && isBundleFilter(value, true);
|
|
678
|
+
};
|
|
679
|
+
var validateRow = (model, row, result = false) => {
|
|
680
|
+
try {
|
|
681
|
+
validateBundleEventFields(row);
|
|
682
|
+
if (!isRecord$3(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$3(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabaseAdapterInputError("invalid-data");
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (result) throw new DatabaseAdapterInputError("invalid-result");
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
/**
|
|
689
|
+
* Downloads, applies, recoveries, failed updates, and the UNCHANGED rows
|
|
690
|
+
* kept for a change belong in installation history.
|
|
691
|
+
*/
|
|
692
|
+
var isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED" || event.type === "UNCHANGED";
|
|
693
|
+
var matchesInsightsEventFilter = (event, filter) => {
|
|
694
|
+
if (filter.kind === "all") return true;
|
|
695
|
+
if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
|
|
696
|
+
return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && bundleRefsOf(event).includes(bundleRefOfFilter(filter));
|
|
697
|
+
};
|
|
698
|
+
var invalidQuery = () => {
|
|
699
|
+
throw new DatabaseAdapterInputError("invalid-query");
|
|
700
|
+
};
|
|
701
|
+
var invalidResult = () => {
|
|
702
|
+
throw new DatabaseAdapterInputError("invalid-result");
|
|
703
|
+
};
|
|
704
|
+
var validateCount = (count) => isTimestamp(count) ? count : invalidResult();
|
|
705
|
+
var isTimeRange = (value) => isRecord$3(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
|
|
706
|
+
var isReleaseReference = (value) => isRecord$3(value) && hasOnlyKeys(value, [
|
|
707
|
+
"releaseId",
|
|
708
|
+
"platform",
|
|
709
|
+
"channel"
|
|
710
|
+
]) && isIdentity(value.releaseId) && hasScope(value);
|
|
711
|
+
var validateAggregateResult = (value) => {
|
|
712
|
+
const result = isRecord$3(value) ? value : invalidResult();
|
|
713
|
+
if (!isTimestamp(result.measuredAtMs)) invalidResult();
|
|
714
|
+
const coverage = result.coverage;
|
|
715
|
+
if (!isRecord$3(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
|
|
716
|
+
};
|
|
717
|
+
/** Validate custom and bundled providers at the same public boundary. */
|
|
718
|
+
var createValidatedInsightsModel = (model) => ({
|
|
719
|
+
async recordEvent(input) {
|
|
720
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, ["event"])) throw new DatabaseAdapterInputError("invalid-data");
|
|
721
|
+
validateRow("bundle_events", input.event);
|
|
722
|
+
await model.recordEvent(input);
|
|
723
|
+
},
|
|
724
|
+
async listEvents(input) {
|
|
725
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
726
|
+
"filter",
|
|
727
|
+
"sinceMs",
|
|
728
|
+
"beforeReceivedAtMs",
|
|
729
|
+
"after",
|
|
730
|
+
"limit"
|
|
731
|
+
]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
|
|
732
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
733
|
+
if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$3(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
|
|
734
|
+
const rows = await model.listEvents(input);
|
|
735
|
+
if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
|
|
736
|
+
let previous = input.after;
|
|
737
|
+
for (const row of rows) {
|
|
738
|
+
validateRow("bundle_events", row, true);
|
|
739
|
+
if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
|
|
740
|
+
previous = {
|
|
741
|
+
receivedAtMs: row.received_at_ms,
|
|
742
|
+
id: row.id
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return rows;
|
|
746
|
+
},
|
|
747
|
+
async findLatestEvents(input) {
|
|
748
|
+
if (!isRecord$3(input)) invalidQuery();
|
|
749
|
+
if ("installId" in input) {
|
|
750
|
+
if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
|
|
751
|
+
} else if (!hasOnlyKeys(input, [
|
|
752
|
+
"userId",
|
|
753
|
+
"afterInstallId",
|
|
754
|
+
"limit"
|
|
755
|
+
]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
|
|
756
|
+
const rows = await model.findLatestEvents(input);
|
|
757
|
+
if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
|
|
758
|
+
let previous = "installId" in input ? void 0 : input.afterInstallId;
|
|
759
|
+
for (const row of rows) {
|
|
760
|
+
validateRow("bundle_events", row, true);
|
|
761
|
+
if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
|
|
762
|
+
previous = row.install_id;
|
|
763
|
+
}
|
|
764
|
+
return rows;
|
|
765
|
+
},
|
|
766
|
+
async countLatestEvents(input) {
|
|
767
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
768
|
+
"platform",
|
|
769
|
+
"channel",
|
|
770
|
+
"sinceMs",
|
|
771
|
+
"bundle"
|
|
772
|
+
]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$3(bundle) || !hasOnlyKeys(bundle, [
|
|
773
|
+
"field",
|
|
774
|
+
"value",
|
|
775
|
+
"types"
|
|
776
|
+
]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
|
|
777
|
+
"UNCHANGED",
|
|
778
|
+
"UPDATE_DOWNLOADED",
|
|
779
|
+
"UPDATE_APPLIED",
|
|
780
|
+
"RECOVERED"
|
|
781
|
+
].includes(type))))) invalidQuery();
|
|
782
|
+
return validateCount(await model.countLatestEvents(input));
|
|
783
|
+
},
|
|
784
|
+
async countEvents(input) {
|
|
785
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
786
|
+
"filter",
|
|
787
|
+
"sinceMs",
|
|
788
|
+
"beforeReceivedAtMs"
|
|
789
|
+
]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
|
|
790
|
+
return validateCount(await model.countEvents(input));
|
|
791
|
+
},
|
|
792
|
+
async countEventSeries(input) {
|
|
793
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
794
|
+
"filter",
|
|
795
|
+
"timeRange",
|
|
796
|
+
"intervalMs"
|
|
797
|
+
]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
|
|
798
|
+
const { start, end } = input.timeRange;
|
|
799
|
+
const { intervalMs } = input;
|
|
800
|
+
if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
|
|
801
|
+
const points = await model.countEventSeries(input);
|
|
802
|
+
if (!Array.isArray(points) || points.length !== (end - start) / intervalMs || points.some((point, index) => !isRecord$3(point) || !hasOnlyKeys(point, ["startMs", "events"]) || point.startMs !== start + index * intervalMs || !isTimestamp(point.events))) invalidResult();
|
|
803
|
+
return points;
|
|
804
|
+
},
|
|
805
|
+
async getReleaseActivity(input) {
|
|
806
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, ["releases"]) || !Array.isArray(input.releases) || input.releases.length < 1 || input.releases.length > 100 || !input.releases.every(isReleaseReference)) invalidQuery();
|
|
807
|
+
const result = await model.getReleaseActivity(input);
|
|
808
|
+
if (!isRecord$3(result) || !isTimestamp(result.measuredAtMs) || !Array.isArray(result.data) || result.data.some((item) => !isRecord$3(item) || !isReleaseReference(item.release) || !isRecord$3(item.metrics) || !isTimestamp(item.metrics.downloads) || !isTimestamp(item.metrics.applies) || !isTimestamp(item.metrics.failedLaunches))) invalidResult();
|
|
809
|
+
return result;
|
|
810
|
+
},
|
|
811
|
+
async getAppUsage(input) {
|
|
812
|
+
if (!isRecord$3(input) || !hasOnlyKeys(input, [
|
|
813
|
+
"channel",
|
|
814
|
+
"platform",
|
|
815
|
+
"appVersion",
|
|
816
|
+
"timeRange",
|
|
817
|
+
"intervalMs"
|
|
818
|
+
]) || !isText(input.channel) || ![
|
|
819
|
+
"all",
|
|
820
|
+
"ios",
|
|
821
|
+
"android"
|
|
822
|
+
].includes(input.platform) || input.appVersion !== void 0 && !isText(input.appVersion) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs) || input.intervalMs < 36e5) invalidQuery();
|
|
823
|
+
const result = await model.getAppUsage(input);
|
|
824
|
+
validateAggregateResult(result);
|
|
825
|
+
if (!isTimestamp(result.activeInstallations) || !Array.isArray(result.points) || !Array.isArray(result.appVersions) || !Array.isArray(result.versions) || !Array.isArray(result.platforms) || !Array.isArray(result.bundleDistribution)) invalidResult();
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
/**
|
|
830
|
+
* The plugin's API as `InsightsModel`, validated at its boundary: what the
|
|
831
|
+
* Insights routes, the console, and the e2e harness read through.
|
|
832
|
+
*/
|
|
833
|
+
var createInsightsModel = (api) => createValidatedInsightsModel({
|
|
834
|
+
recordEvent: ({ event }) => api.recordEvent(event),
|
|
835
|
+
listEvents: api.listEvents,
|
|
836
|
+
findLatestEvents: api.findLatestEvents,
|
|
837
|
+
countLatestEvents: api.countLatestEvents,
|
|
838
|
+
countEvents: api.countEvents,
|
|
839
|
+
countEventSeries: api.countEventSeries,
|
|
840
|
+
getReleaseActivity: api.getReleaseActivity,
|
|
841
|
+
getAppUsage: api.getAppUsage
|
|
842
|
+
});
|
|
843
|
+
/**
|
|
844
|
+
* What Insights' API throws for an input it refuses, such as an unknown
|
|
845
|
+
* event field or a malformed cursor; its routes answer 400 for it.
|
|
846
|
+
*/
|
|
847
|
+
var InsightsBadRequestError = class extends Error {
|
|
848
|
+
name = "InsightsBadRequestError";
|
|
849
|
+
};
|
|
850
|
+
var MAX_EVENT_STRING_LENGTH = 1024;
|
|
851
|
+
var MAX_IDENTITY_LENGTH$1 = 255;
|
|
852
|
+
function isRecord$2(value) {
|
|
853
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
854
|
+
}
|
|
855
|
+
function requireStringField(payload, key) {
|
|
856
|
+
const value = payload[key];
|
|
857
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
858
|
+
return value;
|
|
859
|
+
}
|
|
860
|
+
function requireNullableStringField(payload, key) {
|
|
861
|
+
if (payload[key] === null) return null;
|
|
862
|
+
return requireStringField(payload, key);
|
|
863
|
+
}
|
|
864
|
+
function requireIdentityField(payload, key) {
|
|
865
|
+
const value = requireStringField(payload, key);
|
|
866
|
+
if (value.length > MAX_IDENTITY_LENGTH$1) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
867
|
+
return value;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* The report's idempotency key, when the client sends one: it creates the
|
|
871
|
+
* UUIDv7 once and repeats it on every retry of that report.
|
|
872
|
+
*/
|
|
873
|
+
function readEventId(payload) {
|
|
874
|
+
const value = payload.eventId;
|
|
875
|
+
if (value === void 0) return void 0;
|
|
876
|
+
if (!isUUIDv7(value)) throw new InsightsBadRequestError("Invalid event field: eventId");
|
|
877
|
+
return value;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* The native build's built-in bundle ID: a bundle ID's length at most, and
|
|
881
|
+
* left out when it is missing or malformed, so the report still records.
|
|
882
|
+
*/
|
|
883
|
+
var readMinBundleId = (value) => typeof value === "string" && value.length > 0 && value.length <= 36 ? value : null;
|
|
884
|
+
/** One value of an open set: any other value reads as `unknown`. */
|
|
885
|
+
var oneOf = (values, value) => values.includes(value) ? value : "unknown";
|
|
886
|
+
var STAGES = [
|
|
887
|
+
"check",
|
|
888
|
+
"download",
|
|
889
|
+
"install"
|
|
890
|
+
];
|
|
891
|
+
var REASONS = [
|
|
892
|
+
"network",
|
|
893
|
+
"http",
|
|
894
|
+
"invalid_response",
|
|
895
|
+
"hash_mismatch",
|
|
896
|
+
"signature",
|
|
897
|
+
"patch",
|
|
898
|
+
"extract",
|
|
899
|
+
"storage"
|
|
900
|
+
];
|
|
901
|
+
var RESOURCES = [
|
|
902
|
+
"catalog",
|
|
903
|
+
"artifact",
|
|
904
|
+
"manifest",
|
|
905
|
+
"file",
|
|
906
|
+
"patch",
|
|
907
|
+
"archive"
|
|
908
|
+
];
|
|
909
|
+
var TRANSPORTS = [
|
|
910
|
+
"timeout",
|
|
911
|
+
"dns",
|
|
912
|
+
"tls",
|
|
913
|
+
"connection",
|
|
914
|
+
"offline",
|
|
915
|
+
"cancelled"
|
|
916
|
+
];
|
|
917
|
+
var DELIVERIES = [
|
|
918
|
+
"patch",
|
|
919
|
+
"manifest",
|
|
920
|
+
"archive"
|
|
921
|
+
];
|
|
922
|
+
/** A storage error code: letters, digits, and `._-`. */
|
|
923
|
+
var CODE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
924
|
+
var readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
|
|
925
|
+
var readMetadata = (payload) => isRecord$2(payload.metadata) ? payload.metadata : {};
|
|
926
|
+
/**
|
|
927
|
+
* What an UPDATE_FAILED report says failed. It never refuses the report: an
|
|
928
|
+
* unknown value of a set reads as `unknown`, a missing or malformed failure
|
|
929
|
+
* as an unknown stage and reason, and a malformed optional field is left out,
|
|
930
|
+
* as is a null one.
|
|
931
|
+
*/
|
|
932
|
+
function readFailure(value) {
|
|
933
|
+
const failure = isRecord$2(value) ? value : {};
|
|
934
|
+
const httpStatus = failure.httpStatus;
|
|
935
|
+
const originCode = readCode(failure.originCode);
|
|
936
|
+
const errorMessage = typeof failure.errorMessage === "string" && failure.errorMessage.length <= 2048 ? failure.errorMessage : void 0;
|
|
937
|
+
const errorStack = typeof failure.errorStack === "string" && failure.errorStack.length <= 4096 ? failure.errorStack : void 0;
|
|
938
|
+
return {
|
|
939
|
+
stage: oneOf(STAGES, failure.stage),
|
|
940
|
+
reason: oneOf(REASONS, failure.reason),
|
|
941
|
+
...failure.resource == null ? {} : { resource: oneOf(RESOURCES, failure.resource) },
|
|
942
|
+
...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
|
|
943
|
+
...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
|
|
944
|
+
...originCode === void 0 ? {} : { originCode },
|
|
945
|
+
...errorMessage === void 0 ? {} : { errorMessage },
|
|
946
|
+
...errorStack === void 0 ? {} : { errorStack }
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function readHttpResponse(value) {
|
|
950
|
+
if (!isRecord$2(value) || value.resource !== "catalog" && value.resource !== "artifact" || typeof value.path !== "string" || value.path.length > 1024 || !Number.isSafeInteger(value.status) || value.status < 100 || value.status > 599 || value.body !== null && (typeof value.body !== "string" || new TextEncoder().encode(JSON.stringify(value.body)).byteLength > 4096) || typeof value.bodyTruncated !== "boolean" || !Number.isSafeInteger(value.receivedAtMs) || value.receivedAtMs < 0) throw new InsightsBadRequestError("Invalid HTTP response details");
|
|
951
|
+
return {
|
|
952
|
+
resource: value.resource,
|
|
953
|
+
path: value.path,
|
|
954
|
+
status: value.status,
|
|
955
|
+
body: value.body,
|
|
956
|
+
bodyTruncated: value.bodyTruncated,
|
|
957
|
+
receivedAtMs: value.receivedAtMs
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function requireEvent(payload) {
|
|
961
|
+
if (!isRecord$2(payload)) throw new InsightsBadRequestError("Invalid event payload");
|
|
962
|
+
const platform = requireStringField(payload, "platform");
|
|
963
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
|
|
964
|
+
const eventId = readEventId(payload);
|
|
965
|
+
const metadata = readMetadata(payload);
|
|
966
|
+
const httpMetadata = metadata.httpResponse === void 0 ? {} : { httpResponse: readHttpResponse(metadata.httpResponse) };
|
|
967
|
+
const base = {
|
|
968
|
+
...metadata.httpResponse === void 0 ? {} : { metadata: httpMetadata },
|
|
969
|
+
...eventId === void 0 ? {} : { eventId },
|
|
970
|
+
installId: requireIdentityField(payload, "installId"),
|
|
971
|
+
toBundleId: requireStringField(payload, "toBundleId"),
|
|
972
|
+
...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
|
|
973
|
+
platform,
|
|
974
|
+
appVersion: requireStringField(payload, "appVersion"),
|
|
975
|
+
channel: requireStringField(payload, "channel"),
|
|
976
|
+
cohort: requireStringField(payload, "cohort"),
|
|
977
|
+
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
978
|
+
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
979
|
+
minBundleId: readMinBundleId(payload.minBundleId),
|
|
980
|
+
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
981
|
+
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
982
|
+
};
|
|
983
|
+
const type = requireStringField(payload, "type");
|
|
984
|
+
const movement = () => {
|
|
985
|
+
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
986
|
+
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
987
|
+
return {
|
|
988
|
+
...base,
|
|
989
|
+
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
990
|
+
updateStrategy
|
|
991
|
+
};
|
|
992
|
+
};
|
|
993
|
+
switch (type) {
|
|
994
|
+
case "UPDATE_DOWNLOADED": {
|
|
995
|
+
const delivery = metadata.delivery;
|
|
996
|
+
const read = {
|
|
997
|
+
...httpMetadata,
|
|
998
|
+
...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
|
|
999
|
+
...metadata.patchFallback === true ? { patchFallback: true } : {}
|
|
1000
|
+
};
|
|
1001
|
+
return {
|
|
1002
|
+
...movement(),
|
|
1003
|
+
type,
|
|
1004
|
+
...Object.keys(read).length === 0 ? {} : { metadata: read }
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
case "UPDATE_APPLIED":
|
|
1008
|
+
case "RECOVERED": return {
|
|
1009
|
+
...movement(),
|
|
1010
|
+
type
|
|
1011
|
+
};
|
|
1012
|
+
case "UPDATE_FAILED": return {
|
|
1013
|
+
...movement(),
|
|
1014
|
+
type,
|
|
1015
|
+
metadata: {
|
|
1016
|
+
...httpMetadata,
|
|
1017
|
+
failure: readFailure(metadata.failure)
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
case "UNCHANGED":
|
|
1021
|
+
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
1022
|
+
return {
|
|
1023
|
+
...base,
|
|
1024
|
+
type,
|
|
1025
|
+
fromBundleId: null,
|
|
1026
|
+
updateStrategy: null
|
|
1027
|
+
};
|
|
1028
|
+
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function createBundleEventRow(input) {
|
|
1032
|
+
input = requireEvent(input);
|
|
1033
|
+
const base = {
|
|
1034
|
+
app_version: input.appVersion,
|
|
1035
|
+
channel: input.channel,
|
|
1036
|
+
from_release_id: input.fromReleaseId,
|
|
1037
|
+
id: input.eventId ?? createUUIDv7(),
|
|
1038
|
+
install_id: input.installId,
|
|
1039
|
+
platform: input.platform,
|
|
1040
|
+
received_at_ms: Date.now(),
|
|
1041
|
+
to_bundle_id: input.toBundleId,
|
|
1042
|
+
to_release_id: input.toReleaseId,
|
|
1043
|
+
user_id: input.userId ?? null
|
|
1044
|
+
};
|
|
1045
|
+
const response = input.metadata?.httpResponse;
|
|
1046
|
+
const metadata = {
|
|
1047
|
+
...response ? { http_response: {
|
|
1048
|
+
resource: response.resource,
|
|
1049
|
+
path: response.path,
|
|
1050
|
+
status: response.status,
|
|
1051
|
+
body: response.body,
|
|
1052
|
+
body_truncated: response.bodyTruncated,
|
|
1053
|
+
received_at_ms: response.receivedAtMs
|
|
1054
|
+
} } : {},
|
|
1055
|
+
cohort: input.cohort,
|
|
1056
|
+
fingerprint_hash: input.fingerprintHash,
|
|
1057
|
+
sdk_version: input.sdkVersion ?? null,
|
|
1058
|
+
...input.minBundleId ? { min_bundle_id: input.minBundleId } : {},
|
|
1059
|
+
update_strategy: input.updateStrategy
|
|
1060
|
+
};
|
|
1061
|
+
switch (input.type) {
|
|
1062
|
+
case "UPDATE_DOWNLOADED": {
|
|
1063
|
+
const { delivery, patchFallback } = input.metadata ?? {};
|
|
1064
|
+
return {
|
|
1065
|
+
...base,
|
|
1066
|
+
from_bundle_id: input.fromBundleId,
|
|
1067
|
+
type: input.type,
|
|
1068
|
+
metadata: {
|
|
1069
|
+
...metadata,
|
|
1070
|
+
...delivery === void 0 ? {} : { delivery },
|
|
1071
|
+
...patchFallback ? { patch_fallback: true } : {}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
case "UPDATE_APPLIED":
|
|
1076
|
+
case "RECOVERED": return {
|
|
1077
|
+
...base,
|
|
1078
|
+
from_bundle_id: input.fromBundleId,
|
|
1079
|
+
type: input.type,
|
|
1080
|
+
metadata
|
|
1081
|
+
};
|
|
1082
|
+
case "UPDATE_FAILED": {
|
|
1083
|
+
const { failure } = input.metadata;
|
|
1084
|
+
return {
|
|
1085
|
+
...base,
|
|
1086
|
+
from_bundle_id: input.fromBundleId,
|
|
1087
|
+
type: input.type,
|
|
1088
|
+
metadata: {
|
|
1089
|
+
...metadata,
|
|
1090
|
+
failure: {
|
|
1091
|
+
stage: failure.stage,
|
|
1092
|
+
reason: failure.reason,
|
|
1093
|
+
...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
|
|
1094
|
+
...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
|
|
1095
|
+
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
1096
|
+
...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
|
|
1097
|
+
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
1098
|
+
...failure.originCode === void 0 ? {} : { origin_code: failure.originCode }
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
case "UNCHANGED": return {
|
|
1104
|
+
...base,
|
|
1105
|
+
from_bundle_id: null,
|
|
1106
|
+
type: input.type,
|
|
1107
|
+
metadata
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
var DEFAULT_PAGE_LIMIT = 50;
|
|
1112
|
+
var MAX_PAGE_LIMIT = 100;
|
|
1113
|
+
var MAX_EVENT_ID_LENGTH = 1024;
|
|
1114
|
+
var MAX_IDENTITY_LENGTH = 255;
|
|
1115
|
+
var MAX_CURSOR_LENGTH = 8192;
|
|
1116
|
+
var HOUR_MS = 36e5;
|
|
1117
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
1118
|
+
var WINDOW_MS = {
|
|
1119
|
+
"24h": 864e5,
|
|
1120
|
+
"7d": 6048e5,
|
|
1121
|
+
"30d": 2592e6
|
|
1122
|
+
};
|
|
1123
|
+
/**
|
|
1124
|
+
* The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
|
|
1125
|
+
* bundle event list, and the range one covers without `sinceMs`: raw events
|
|
1126
|
+
* are listed inside a time range, as analytics products list them. It is a
|
|
1127
|
+
* duration, so a range that ends mid-day touches 91 UTC days. Installation
|
|
1128
|
+
* history is one index range and reads back to `sinceMs`.
|
|
1129
|
+
*/
|
|
1130
|
+
var EVENT_LIST_RANGE_MS = 2160 * HOUR_MS;
|
|
1131
|
+
var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1132
|
+
var requireString = (value, label, maximumLength) => {
|
|
1133
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
1134
|
+
return value;
|
|
1135
|
+
};
|
|
1136
|
+
var requireTimestamp = (value, label) => {
|
|
1137
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
1138
|
+
return Number(value);
|
|
1139
|
+
};
|
|
1140
|
+
var readLimit = (value) => {
|
|
1141
|
+
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
1142
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
|
|
1143
|
+
return limit;
|
|
1144
|
+
};
|
|
1145
|
+
var encodeBase64Url = (value) => {
|
|
1146
|
+
const bytes = new TextEncoder().encode(value);
|
|
1147
|
+
let binary = "";
|
|
1148
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1149
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
1150
|
+
};
|
|
1151
|
+
var decodeBase64Url = (value) => {
|
|
1152
|
+
if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
1153
|
+
try {
|
|
1154
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
1155
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
1156
|
+
const binary = atob(padded);
|
|
1157
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1158
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1159
|
+
} catch {
|
|
1160
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
var encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
|
|
1164
|
+
var decodeCursor = (value) => {
|
|
1165
|
+
try {
|
|
1166
|
+
return JSON.parse(decodeBase64Url(value));
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (error instanceof InsightsBadRequestError) throw error;
|
|
1169
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var readScope = (input) => {
|
|
1173
|
+
if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
|
|
1174
|
+
return {
|
|
1175
|
+
platform: input.platform,
|
|
1176
|
+
channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
|
|
1177
|
+
};
|
|
1178
|
+
};
|
|
1179
|
+
var bundleFilter = (input) => {
|
|
1180
|
+
const scope = readScope(input);
|
|
1181
|
+
const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
1182
|
+
switch (input.outcome) {
|
|
1183
|
+
case "downloaded": return {
|
|
1184
|
+
...scope,
|
|
1185
|
+
type: "UPDATE_DOWNLOADED",
|
|
1186
|
+
toBundleId: bundleId
|
|
1187
|
+
};
|
|
1188
|
+
case "applied": return {
|
|
1189
|
+
...scope,
|
|
1190
|
+
type: "UPDATE_APPLIED",
|
|
1191
|
+
toBundleId: bundleId
|
|
1192
|
+
};
|
|
1193
|
+
case "recovered": return {
|
|
1194
|
+
...scope,
|
|
1195
|
+
type: "RECOVERED",
|
|
1196
|
+
fromBundleId: bundleId
|
|
1197
|
+
};
|
|
1198
|
+
case "failed": return {
|
|
1199
|
+
...scope,
|
|
1200
|
+
type: "UPDATE_FAILED",
|
|
1201
|
+
toBundleId: bundleId
|
|
1202
|
+
};
|
|
1203
|
+
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
var sameFilter = (left, right) => {
|
|
1207
|
+
if (!isRecord$1(left) || left.kind !== right.kind) return false;
|
|
1208
|
+
switch (right.kind) {
|
|
1209
|
+
case "all": return true;
|
|
1210
|
+
case "installationMovement": return left.installId === right.installId;
|
|
1211
|
+
case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
var readEventCursor = (value, filter) => {
|
|
1215
|
+
const cursor = decodeCursor(value);
|
|
1216
|
+
if (!isRecord$1(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord$1(cursor.filter) || !isRecord$1(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
1217
|
+
if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
|
|
1218
|
+
if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
|
|
1219
|
+
return {
|
|
1220
|
+
after: {
|
|
1221
|
+
id: cursor.after.id,
|
|
1222
|
+
receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
|
|
1223
|
+
},
|
|
1224
|
+
beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
|
|
1225
|
+
kind: "events",
|
|
1226
|
+
sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
|
|
1227
|
+
filter,
|
|
1228
|
+
version: 2
|
|
1229
|
+
};
|
|
1230
|
+
};
|
|
1231
|
+
var readUserInstallationCursor = (value, userId) => {
|
|
1232
|
+
const cursor = decodeCursor(value);
|
|
1233
|
+
if (!isRecord$1(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
|
|
1234
|
+
return {
|
|
1235
|
+
afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
|
|
1236
|
+
kind: "user-installations",
|
|
1237
|
+
userId,
|
|
1238
|
+
version: 1
|
|
1239
|
+
};
|
|
1240
|
+
};
|
|
1241
|
+
var compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
|
|
1242
|
+
var isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
|
|
1243
|
+
var assertEventRows = (rows, input) => {
|
|
1244
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
|
|
1245
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
1246
|
+
const row = rows[index];
|
|
1247
|
+
const previous = rows[index - 1];
|
|
1248
|
+
if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
/** An `UPDATE_FAILED` row's failure, as the event lists show it. */
|
|
1252
|
+
var toFailure = (failure) => ({
|
|
1253
|
+
stage: failure.stage,
|
|
1254
|
+
reason: failure.reason,
|
|
1255
|
+
...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
|
|
1256
|
+
...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
|
|
1257
|
+
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
1258
|
+
...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
|
|
1259
|
+
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
1260
|
+
...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code }
|
|
1261
|
+
});
|
|
1262
|
+
/** What a row's metadata adds for its type: a failure or a delivery. */
|
|
1263
|
+
var eventDetails = ({ type, metadata }) => ({
|
|
1264
|
+
...metadata.http_response !== void 0 ? { httpResponse: {
|
|
1265
|
+
resource: metadata.http_response.resource,
|
|
1266
|
+
path: metadata.http_response.path,
|
|
1267
|
+
status: metadata.http_response.status,
|
|
1268
|
+
body: metadata.http_response.body,
|
|
1269
|
+
bodyTruncated: metadata.http_response.body_truncated,
|
|
1270
|
+
receivedAtMs: metadata.http_response.received_at_ms
|
|
1271
|
+
} } : {},
|
|
1272
|
+
...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
|
|
1273
|
+
...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
|
|
1274
|
+
...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
|
|
1275
|
+
...type === "UNCHANGED" && metadata.change !== void 0 ? { change: {
|
|
1276
|
+
kinds: metadata.change.kinds,
|
|
1277
|
+
...metadata.change.previous === null ? {} : { previous: {
|
|
1278
|
+
bundleId: metadata.change.previous.bundle_id,
|
|
1279
|
+
releaseId: metadata.change.previous.release_id,
|
|
1280
|
+
appVersion: metadata.change.previous.app_version,
|
|
1281
|
+
channel: metadata.change.previous.channel,
|
|
1282
|
+
...metadata.change.previous.min_bundle_id === void 0 ? {} : { minBundleId: metadata.change.previous.min_bundle_id }
|
|
1283
|
+
} }
|
|
1284
|
+
} } : {},
|
|
1285
|
+
...metadata.late === true ? { late: true } : {},
|
|
1286
|
+
...metadata.implied_download === true ? { impliedDownload: true } : {}
|
|
1287
|
+
});
|
|
1288
|
+
var toEventHistoryRow = (row) => ({
|
|
1289
|
+
...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
|
|
1290
|
+
...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
|
|
1291
|
+
...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
|
|
1292
|
+
appVersion: row.app_version,
|
|
1293
|
+
channel: row.channel,
|
|
1294
|
+
cohort: row.metadata.cohort,
|
|
1295
|
+
fromBundleId: row.from_bundle_id,
|
|
1296
|
+
id: row.id,
|
|
1297
|
+
installId: row.install_id,
|
|
1298
|
+
platform: row.platform,
|
|
1299
|
+
receivedAtMs: row.received_at_ms,
|
|
1300
|
+
toBundleId: row.to_bundle_id,
|
|
1301
|
+
type: row.type,
|
|
1302
|
+
userId: row.user_id,
|
|
1303
|
+
...eventDetails(row)
|
|
1304
|
+
});
|
|
1305
|
+
var toInstallationRow = (row) => ({
|
|
1306
|
+
...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
|
|
1307
|
+
...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
|
|
1308
|
+
appVersion: row.app_version,
|
|
1309
|
+
channel: row.channel,
|
|
1310
|
+
cohort: row.metadata.cohort,
|
|
1311
|
+
installId: row.install_id,
|
|
1312
|
+
lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
|
|
1313
|
+
pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
|
|
1314
|
+
pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
|
|
1315
|
+
latestStatus: row.type,
|
|
1316
|
+
platform: row.platform,
|
|
1317
|
+
receivedAtMs: row.received_at_ms,
|
|
1318
|
+
userId: row.user_id
|
|
1319
|
+
});
|
|
1320
|
+
/**
|
|
1321
|
+
* Where a list without `sinceMs` starts: 90 days before its cutoff for the
|
|
1322
|
+
* global and bundle lists, at the first event for installation history.
|
|
1323
|
+
*/
|
|
1324
|
+
var defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
|
|
1325
|
+
/** Whether a cursor's row lies inside the list's range. */
|
|
1326
|
+
var isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
|
|
1327
|
+
/** A page's range: its cursor's, which the request may repeat but not change. */
|
|
1328
|
+
var readEventBounds = (input, cursor, filter) => {
|
|
1329
|
+
const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
|
|
1330
|
+
if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
|
|
1331
|
+
const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
|
|
1332
|
+
const bounds = {
|
|
1333
|
+
beforeReceivedAtMs,
|
|
1334
|
+
sinceMs
|
|
1335
|
+
};
|
|
1336
|
+
if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && !isInsideBounds(cursor, bounds)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
|
|
1337
|
+
if (filter.kind !== "installationMovement" && beforeReceivedAtMs - sinceMs > 7776e6) throw new InsightsBadRequestError("Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.");
|
|
1338
|
+
return bounds;
|
|
1339
|
+
};
|
|
1340
|
+
var pageEventRows = async (model, input, filter, map) => {
|
|
1341
|
+
const limit = readLimit(input.limit);
|
|
1342
|
+
const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
|
|
1343
|
+
const bounds = readEventBounds(input, cursor, filter);
|
|
1344
|
+
const query = {
|
|
1345
|
+
filter,
|
|
1346
|
+
...bounds,
|
|
1347
|
+
...cursor === void 0 ? {} : { after: cursor.after },
|
|
1348
|
+
limit
|
|
1349
|
+
};
|
|
1350
|
+
const rows = await model.listEvents(query);
|
|
1351
|
+
assertEventRows(rows, query);
|
|
1352
|
+
const last = rows.at(-1);
|
|
1353
|
+
return {
|
|
1354
|
+
beforeReceivedAtMs: bounds.beforeReceivedAtMs,
|
|
1355
|
+
data: rows.map(map),
|
|
1356
|
+
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
1357
|
+
after: {
|
|
1358
|
+
id: last.id,
|
|
1359
|
+
receivedAtMs: last.received_at_ms
|
|
1360
|
+
},
|
|
1361
|
+
...bounds,
|
|
1362
|
+
kind: "events",
|
|
1363
|
+
filter,
|
|
1364
|
+
version: 2
|
|
1365
|
+
}) : null
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1368
|
+
var assertInstallationRows = (rows, input) => {
|
|
1369
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
|
|
1370
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
1371
|
+
const row = rows[index];
|
|
1372
|
+
const previous = rows[index - 1];
|
|
1373
|
+
if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
var createInsightsProvider = (model) => Object.freeze({
|
|
1377
|
+
async appendBundleEvent(input) {
|
|
1378
|
+
const event = createBundleEventRow(input);
|
|
1379
|
+
await model.recordEvent({ event });
|
|
1380
|
+
},
|
|
1381
|
+
listEvents(input) {
|
|
1382
|
+
return pageEventRows(model, input, input.bundle === void 0 ? { kind: "all" } : {
|
|
1383
|
+
kind: "bundle",
|
|
1384
|
+
...bundleFilter(input.bundle)
|
|
1385
|
+
}, toEventHistoryRow);
|
|
1386
|
+
},
|
|
1387
|
+
async listInstallationEvents(input) {
|
|
1388
|
+
if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
|
|
1389
|
+
return pageEventRows(model, input, {
|
|
1390
|
+
kind: "installationMovement",
|
|
1391
|
+
installId: requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH)
|
|
1392
|
+
}, (row) => toEventHistoryRow(row));
|
|
1393
|
+
},
|
|
1394
|
+
async getInstallation({ installId }) {
|
|
1395
|
+
const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
1396
|
+
const rows = await model.findLatestEvents({ installId: normalizedInstallId });
|
|
1397
|
+
const row = rows[0] ?? null;
|
|
1398
|
+
if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
|
|
1399
|
+
return row === null ? null : toInstallationRow(row);
|
|
1400
|
+
},
|
|
1401
|
+
async pageInstallationsByCurrentUserId(input) {
|
|
1402
|
+
const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
|
|
1403
|
+
const limit = readLimit(input.limit);
|
|
1404
|
+
const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
|
|
1405
|
+
const databaseInput = {
|
|
1406
|
+
userId,
|
|
1407
|
+
...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
|
|
1408
|
+
limit
|
|
1409
|
+
};
|
|
1410
|
+
const rows = await model.findLatestEvents(databaseInput);
|
|
1411
|
+
assertInstallationRows(rows, databaseInput);
|
|
1412
|
+
const last = rows.at(-1);
|
|
1413
|
+
return {
|
|
1414
|
+
data: rows.map(toInstallationRow),
|
|
1415
|
+
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
1416
|
+
afterInstallId: last.install_id,
|
|
1417
|
+
kind: "user-installations",
|
|
1418
|
+
userId,
|
|
1419
|
+
version: 1
|
|
1420
|
+
}) : null
|
|
1421
|
+
};
|
|
1422
|
+
},
|
|
1423
|
+
async getReportingOverview(input) {
|
|
1424
|
+
const scope = readScope(input);
|
|
1425
|
+
const { window } = input;
|
|
1426
|
+
if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
|
|
1427
|
+
const now = Date.now();
|
|
1428
|
+
const beforeReceivedAtMs = now - now % HOUR_MS + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
|
|
1429
|
+
const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
|
|
1430
|
+
const sinceMs = reach - reach % DAY_MS;
|
|
1431
|
+
const measure = async (count) => {
|
|
1432
|
+
const value = await count;
|
|
1433
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
|
|
1434
|
+
return {
|
|
1435
|
+
count: value,
|
|
1436
|
+
measuredAtMs: Date.now()
|
|
1437
|
+
};
|
|
1438
|
+
};
|
|
1439
|
+
const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
1440
|
+
const reporting = measure(model.countLatestEvents({
|
|
1441
|
+
...scope,
|
|
1442
|
+
sinceMs
|
|
1443
|
+
}));
|
|
1444
|
+
if (bundleId === void 0) return {
|
|
1445
|
+
...scope,
|
|
1446
|
+
window,
|
|
1447
|
+
sinceMs,
|
|
1448
|
+
beforeReceivedAtMs,
|
|
1449
|
+
reportingInstallations: await reporting
|
|
1450
|
+
};
|
|
1451
|
+
const countOutcome = (outcome) => measure(model.countEvents({
|
|
1452
|
+
filter: bundleFilter({
|
|
1453
|
+
...scope,
|
|
1454
|
+
bundleId,
|
|
1455
|
+
outcome
|
|
1456
|
+
}),
|
|
1457
|
+
sinceMs,
|
|
1458
|
+
beforeReceivedAtMs
|
|
1459
|
+
}));
|
|
1460
|
+
const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
|
|
1461
|
+
reporting,
|
|
1462
|
+
measure(model.countLatestEvents({
|
|
1463
|
+
...scope,
|
|
1464
|
+
sinceMs,
|
|
1465
|
+
bundle: [{
|
|
1466
|
+
field: "from_bundle_id",
|
|
1467
|
+
value: bundleId,
|
|
1468
|
+
types: ["UPDATE_DOWNLOADED"]
|
|
1469
|
+
}, {
|
|
1470
|
+
field: "to_bundle_id",
|
|
1471
|
+
value: bundleId,
|
|
1472
|
+
types: [
|
|
1473
|
+
"UNCHANGED",
|
|
1474
|
+
"UPDATE_APPLIED",
|
|
1475
|
+
"RECOVERED"
|
|
1476
|
+
]
|
|
1477
|
+
}]
|
|
1478
|
+
})),
|
|
1479
|
+
countOutcome("downloaded"),
|
|
1480
|
+
countOutcome("applied"),
|
|
1481
|
+
countOutcome("recovered"),
|
|
1482
|
+
countOutcome("failed")
|
|
1483
|
+
]);
|
|
1484
|
+
return {
|
|
1485
|
+
...scope,
|
|
1486
|
+
window,
|
|
1487
|
+
sinceMs,
|
|
1488
|
+
beforeReceivedAtMs,
|
|
1489
|
+
reportingInstallations,
|
|
1490
|
+
bundle: {
|
|
1491
|
+
bundleId,
|
|
1492
|
+
reportingInstallations: bundleInstallations,
|
|
1493
|
+
downloadedReports,
|
|
1494
|
+
appliedReports,
|
|
1495
|
+
recoveredReports,
|
|
1496
|
+
failedReports
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
30 * DAY_MS$1;
|
|
1502
|
+
/** The reads over the `insights()` API tooling assembled on the server's database. */
|
|
1503
|
+
var createInsightsReads = (api) => ({
|
|
1504
|
+
...createInsightsProvider(createInsightsModel(api)),
|
|
1505
|
+
getRetention: async () => api.retention,
|
|
1506
|
+
getUpdateFailures: api.getUpdateFailures
|
|
1507
|
+
});
|
|
1508
|
+
/** The `error` a failed admin route answered with, or a message naming its status. */
|
|
1509
|
+
var errorOf = async (response, path) => {
|
|
1510
|
+
const body = await response.json().catch(() => null);
|
|
1511
|
+
return new Error(typeof body === "object" && body !== null && typeof Reflect.get(body, "error") === "string" ? String(Reflect.get(body, "error")) : `The server answered ${path.split("?")[0]} with ${response.status}.`);
|
|
1512
|
+
};
|
|
1513
|
+
var withQuery = (path, input) => {
|
|
1514
|
+
const query = new URLSearchParams();
|
|
1515
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0) query.set(key, String(value));
|
|
1516
|
+
const value = query.toString();
|
|
1517
|
+
return value.length === 0 ? path : `${path}?${value}`;
|
|
1518
|
+
};
|
|
1519
|
+
var eventQuery = ({ beforeReceivedAtMs, sinceMs, cursor, limit }) => ({
|
|
1520
|
+
beforeReceivedAtMs,
|
|
1521
|
+
sinceMs,
|
|
1522
|
+
cursor,
|
|
1523
|
+
limit
|
|
1524
|
+
});
|
|
1525
|
+
/**
|
|
1526
|
+
* The reads over a self-hosted server's admin routes, which its `insights()`
|
|
1527
|
+
* serves, for tooling that reaches it through `standaloneRepository`.
|
|
1528
|
+
*/
|
|
1529
|
+
var createInsightsAdminReads = (fetchAdmin, { unavailable = () => /* @__PURE__ */ new Error("The server runs without insights(): its admin API serves no Insights routes.") } = {}) => {
|
|
1530
|
+
const read = async (path) => {
|
|
1531
|
+
const response = await fetchAdmin(path);
|
|
1532
|
+
if (response.status === 404) return null;
|
|
1533
|
+
if (!response.ok) throw await errorOf(response, path);
|
|
1534
|
+
return await response.json();
|
|
1535
|
+
};
|
|
1536
|
+
const required = async (path) => {
|
|
1537
|
+
const value = await read(path);
|
|
1538
|
+
if (value === null) throw unavailable();
|
|
1539
|
+
return value;
|
|
1540
|
+
};
|
|
1541
|
+
return {
|
|
1542
|
+
getReportingOverview: ({ platform, channel, window, bundleId }) => required(withQuery("/overview", {
|
|
1543
|
+
platform,
|
|
1544
|
+
channel,
|
|
1545
|
+
window,
|
|
1546
|
+
bundleId
|
|
1547
|
+
})),
|
|
1548
|
+
listEvents: ({ bundle, ...input }) => required(withQuery("/events", {
|
|
1549
|
+
...eventQuery(input),
|
|
1550
|
+
...bundle === void 0 ? {} : {
|
|
1551
|
+
platform: bundle.platform,
|
|
1552
|
+
channel: bundle.channel,
|
|
1553
|
+
bundleId: bundle.bundleId,
|
|
1554
|
+
outcome: bundle.outcome
|
|
1555
|
+
}
|
|
1556
|
+
})),
|
|
1557
|
+
listInstallationEvents: ({ installId, ...input }) => required(withQuery(`/installations/${encodeURIComponent(installId)}/events`, eventQuery(input))),
|
|
1558
|
+
getInstallation: ({ installId }) => read(`/installations/${encodeURIComponent(installId)}`),
|
|
1559
|
+
pageInstallationsByCurrentUserId: ({ userId, cursor, limit }) => required(withQuery("/installations", {
|
|
1560
|
+
userId,
|
|
1561
|
+
cursor,
|
|
1562
|
+
limit
|
|
1563
|
+
})),
|
|
1564
|
+
getUpdateFailures: ({ platform, channel, releaseId, timeRange }) => required(withQuery("/failures", {
|
|
1565
|
+
platform,
|
|
1566
|
+
channel,
|
|
1567
|
+
releaseId,
|
|
1568
|
+
start: timeRange?.start,
|
|
1569
|
+
end: timeRange?.end
|
|
1570
|
+
})),
|
|
1571
|
+
getRetention: () => required("/retention")
|
|
1572
|
+
};
|
|
1573
|
+
};
|
|
1574
|
+
/**
|
|
1575
|
+
* A record's own entry. Parameter keys and condition names come from users,
|
|
1576
|
+
* so `constructor` or `__proto__` must name theirs, never Object.prototype's.
|
|
1577
|
+
*/
|
|
1578
|
+
var ownEntry = (record, key) => record !== void 0 && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
|
|
1579
|
+
Object.freeze({
|
|
1580
|
+
conditions: Object.freeze([]),
|
|
1581
|
+
parameters: Object.freeze({})
|
|
1582
|
+
});
|
|
1583
|
+
/**
|
|
1584
|
+
* A template's JSON stays under this many characters, so a device's values
|
|
1585
|
+
* always fit the 64 KB a client plugin may store under one key.
|
|
1586
|
+
*/
|
|
1587
|
+
var REMOTE_CONFIG_MAX_TEMPLATE_LENGTH = 6e4;
|
|
1588
|
+
var REMOTE_CONFIG_MAX_PARAMETERS = 2e3;
|
|
1589
|
+
var MAX_RULE_VALUES = 100;
|
|
1590
|
+
var MAX_DESCRIPTION_LENGTH = 256;
|
|
1591
|
+
var MAX_CONDITION_NAME_LENGTH = 100;
|
|
1592
|
+
var MAX_RULE_TEXT_LENGTH = 128;
|
|
1593
|
+
var PARAMETER_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/u;
|
|
1594
|
+
var SEED_PATTERN = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
1595
|
+
/** A date-time with its time zone, so it names one instant. */
|
|
1596
|
+
var DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
1597
|
+
var VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
1598
|
+
"STRING",
|
|
1599
|
+
"NUMBER",
|
|
1600
|
+
"BOOLEAN",
|
|
1601
|
+
"JSON"
|
|
1602
|
+
]);
|
|
1603
|
+
var PLATFORMS = /* @__PURE__ */ new Set(["ios", "android"]);
|
|
1604
|
+
/** A template that cannot be published; `issues` lists every problem found. */
|
|
1605
|
+
var RemoteConfigValidationError = class extends Error {
|
|
1606
|
+
issues;
|
|
1607
|
+
name = "RemoteConfigValidationError";
|
|
1608
|
+
constructor(issues) {
|
|
1609
|
+
super(`The Remote Config template is invalid: ${issues.slice(0, 3).map(({ path, message }) => path ? `${path}: ${message}` : message).join("; ")}${issues.length > 3 ? ` (and ${issues.length - 3} more)` : ""}`);
|
|
1610
|
+
this.issues = issues;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1614
|
+
var visibleText = (value, max) => typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\p{Cc}]/u.test(value);
|
|
1615
|
+
/** Whether a value's text suits its type; the problem otherwise. */
|
|
1616
|
+
var checkValueText = (valueType, text) => {
|
|
1617
|
+
switch (valueType) {
|
|
1618
|
+
case "NUMBER": return text.trim().length > 0 && Number.isFinite(Number(text)) ? null : "must be a number.";
|
|
1619
|
+
case "BOOLEAN": return text === "true" || text === "false" ? null : "must be \"true\" or \"false\".";
|
|
1620
|
+
case "JSON": try {
|
|
1621
|
+
JSON.parse(text);
|
|
1622
|
+
return null;
|
|
1623
|
+
} catch {
|
|
1624
|
+
return "must be valid JSON.";
|
|
1625
|
+
}
|
|
1626
|
+
default: return null;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
/**
|
|
1630
|
+
* Checks a template and returns it with only its declared fields, or throws
|
|
1631
|
+
* `RemoteConfigValidationError` with every problem it finds.
|
|
1632
|
+
*/
|
|
1633
|
+
var validateRemoteConfigTemplate = (input) => {
|
|
1634
|
+
const issues = [];
|
|
1635
|
+
const issue = (path, message) => {
|
|
1636
|
+
issues.push({
|
|
1637
|
+
path,
|
|
1638
|
+
message
|
|
1639
|
+
});
|
|
1640
|
+
};
|
|
1641
|
+
const unknownKeys = (value, allowed, path) => {
|
|
1642
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) issue(path ? `${path}.${key}` : key, "is not a template field.");
|
|
1643
|
+
};
|
|
1644
|
+
if (!isRecord(input)) throw new RemoteConfigValidationError([{
|
|
1645
|
+
path: "",
|
|
1646
|
+
message: "A template is an object."
|
|
1647
|
+
}]);
|
|
1648
|
+
unknownKeys(input, ["conditions", "parameters"], "");
|
|
1649
|
+
const conditions = [];
|
|
1650
|
+
const conditionNames = /* @__PURE__ */ new Set();
|
|
1651
|
+
const rawConditions = input.conditions ?? [];
|
|
1652
|
+
if (!Array.isArray(rawConditions)) issue("conditions", "must be a list.");
|
|
1653
|
+
else {
|
|
1654
|
+
if (rawConditions.length > 500) issue("conditions", `holds at most 500 conditions.`);
|
|
1655
|
+
rawConditions.forEach((raw, index) => {
|
|
1656
|
+
const path = `conditions[${index}]`;
|
|
1657
|
+
if (!isRecord(raw)) {
|
|
1658
|
+
issue(path, "must be an object.");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
unknownKeys(raw, ["name", "rules"], path);
|
|
1662
|
+
const { name } = raw;
|
|
1663
|
+
if (!visibleText(name, MAX_CONDITION_NAME_LENGTH)) issue(`${path}.name`, `must be 1-${MAX_CONDITION_NAME_LENGTH} visible characters.`);
|
|
1664
|
+
else if (conditionNames.has(name)) issue(`${path}.name`, `"${name}" names another condition too.`);
|
|
1665
|
+
else conditionNames.add(name);
|
|
1666
|
+
const rules = validateRules(raw.rules, `${path}.rules`, issue);
|
|
1667
|
+
if (typeof name === "string") conditions.push({
|
|
1668
|
+
name,
|
|
1669
|
+
rules
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const parameters = [];
|
|
1674
|
+
const rawParameters = input.parameters ?? {};
|
|
1675
|
+
if (!isRecord(rawParameters)) issue("parameters", "must be an object keyed by parameter.");
|
|
1676
|
+
else {
|
|
1677
|
+
const keys = Object.keys(rawParameters);
|
|
1678
|
+
if (keys.length > 2e3) issue("parameters", `holds at most ${REMOTE_CONFIG_MAX_PARAMETERS} parameters.`);
|
|
1679
|
+
for (const key of keys) {
|
|
1680
|
+
const path = `parameters.${key}`;
|
|
1681
|
+
if (!PARAMETER_KEY_PATTERN.test(key)) issue(path, "Parameter keys start with a letter or _, and hold up to 256 letters, digits, and _.");
|
|
1682
|
+
const raw = rawParameters[key];
|
|
1683
|
+
if (!isRecord(raw)) {
|
|
1684
|
+
issue(path, "must be an object.");
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
unknownKeys(raw, [
|
|
1688
|
+
"valueType",
|
|
1689
|
+
"defaultValue",
|
|
1690
|
+
"conditionalValues",
|
|
1691
|
+
"description"
|
|
1692
|
+
], path);
|
|
1693
|
+
const valueType = raw.valueType;
|
|
1694
|
+
if (typeof valueType !== "string" || !VALUE_TYPES.has(valueType)) {
|
|
1695
|
+
issue(`${path}.valueType`, "must be STRING, NUMBER, BOOLEAN, or JSON.");
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const type = valueType;
|
|
1699
|
+
const readValue = (value, valuePath) => {
|
|
1700
|
+
if (!isRecord(value)) {
|
|
1701
|
+
issue(valuePath, "must be { value } or { useInAppDefault: true }.");
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
if ("useInAppDefault" in value) {
|
|
1705
|
+
unknownKeys(value, ["useInAppDefault"], valuePath);
|
|
1706
|
+
if (value.useInAppDefault !== true) {
|
|
1707
|
+
issue(`${valuePath}.useInAppDefault`, "must be true.");
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
return { useInAppDefault: true };
|
|
1711
|
+
}
|
|
1712
|
+
unknownKeys(value, ["value"], valuePath);
|
|
1713
|
+
if (typeof value.value !== "string") {
|
|
1714
|
+
issue(`${valuePath}.value`, "must be text.");
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
const problem = checkValueText(type, value.value);
|
|
1718
|
+
if (problem !== null) {
|
|
1719
|
+
issue(`${valuePath}.value`, problem);
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
return { value: value.value };
|
|
1723
|
+
};
|
|
1724
|
+
const defaultValue = readValue(raw.defaultValue, `${path}.defaultValue`);
|
|
1725
|
+
const conditionalValues = [];
|
|
1726
|
+
if (raw.conditionalValues !== void 0) if (!isRecord(raw.conditionalValues)) issue(`${path}.conditionalValues`, "must be an object keyed by condition.");
|
|
1727
|
+
else for (const [conditionName, value] of Object.entries(raw.conditionalValues)) {
|
|
1728
|
+
const valuePath = `${path}.conditionalValues.${conditionName}`;
|
|
1729
|
+
if (!conditionNames.has(conditionName)) {
|
|
1730
|
+
issue(valuePath, `No condition is named "${conditionName}".`);
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
const read = readValue(value, valuePath);
|
|
1734
|
+
if (read !== null) conditionalValues.push([conditionName, read]);
|
|
1735
|
+
}
|
|
1736
|
+
let description;
|
|
1737
|
+
if (raw.description !== void 0) {
|
|
1738
|
+
const text = typeof raw.description === "string" ? raw.description.trim() : null;
|
|
1739
|
+
if (text === null || text.length > MAX_DESCRIPTION_LENGTH) issue(`${path}.description`, `must be text of up to ${MAX_DESCRIPTION_LENGTH} characters.`);
|
|
1740
|
+
else if (text.length > 0) description = text;
|
|
1741
|
+
}
|
|
1742
|
+
if (defaultValue !== null) parameters.push([key, {
|
|
1743
|
+
valueType: type,
|
|
1744
|
+
defaultValue,
|
|
1745
|
+
...conditionalValues.length === 0 ? {} : { conditionalValues: Object.fromEntries(conditionalValues) },
|
|
1746
|
+
...description === void 0 ? {} : { description }
|
|
1747
|
+
}]);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (issues.length > 0) throw new RemoteConfigValidationError(issues);
|
|
1751
|
+
const template = {
|
|
1752
|
+
conditions,
|
|
1753
|
+
parameters: Object.fromEntries(parameters)
|
|
1754
|
+
};
|
|
1755
|
+
const length = JSON.stringify(template).length;
|
|
1756
|
+
if (length > 6e4) throw new RemoteConfigValidationError([{
|
|
1757
|
+
path: "",
|
|
1758
|
+
message: `The template takes ${length.toLocaleString("en-US")} characters of JSON; the limit is ${REMOTE_CONFIG_MAX_TEMPLATE_LENGTH.toLocaleString("en-US")}.`
|
|
1759
|
+
}]);
|
|
1760
|
+
return template;
|
|
1761
|
+
};
|
|
1762
|
+
var textList = (value, path, issue, check, describe) => {
|
|
1763
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_RULE_VALUES) {
|
|
1764
|
+
issue(path, `must list 1-${MAX_RULE_VALUES} values.`);
|
|
1765
|
+
return [];
|
|
1766
|
+
}
|
|
1767
|
+
const list = [];
|
|
1768
|
+
value.forEach((entry, index) => {
|
|
1769
|
+
if (typeof entry !== "string" || !check(entry)) issue(`${path}[${index}]`, describe);
|
|
1770
|
+
else if (!list.includes(entry)) list.push(entry);
|
|
1771
|
+
});
|
|
1772
|
+
return list;
|
|
1773
|
+
};
|
|
1774
|
+
var RULE_FIELDS = {
|
|
1775
|
+
platform: ["type", "platforms"],
|
|
1776
|
+
channel: ["type", "channels"],
|
|
1777
|
+
appVersion: ["type", "range"],
|
|
1778
|
+
cohort: ["type", "cohorts"],
|
|
1779
|
+
percent: [
|
|
1780
|
+
"type",
|
|
1781
|
+
"seed",
|
|
1782
|
+
"from",
|
|
1783
|
+
"to"
|
|
1784
|
+
],
|
|
1785
|
+
fingerprint: ["type", "hashes"],
|
|
1786
|
+
dateTime: [
|
|
1787
|
+
"type",
|
|
1788
|
+
"from",
|
|
1789
|
+
"to"
|
|
1790
|
+
]
|
|
1791
|
+
};
|
|
1792
|
+
var validateRules = (value, path, issue) => {
|
|
1793
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1794
|
+
issue(path, "must list at least one rule.");
|
|
1795
|
+
return [];
|
|
1796
|
+
}
|
|
1797
|
+
const rules = [];
|
|
1798
|
+
value.forEach((raw, index) => {
|
|
1799
|
+
const rulePath = `${path}[${index}]`;
|
|
1800
|
+
if (!isRecord(raw) || typeof raw.type !== "string") {
|
|
1801
|
+
issue(rulePath, "must be a rule object with a type.");
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const type = raw.type;
|
|
1805
|
+
const fields = ownEntry(RULE_FIELDS, type);
|
|
1806
|
+
if (fields === void 0) {
|
|
1807
|
+
issue(`${rulePath}.type`, "must be platform, channel, appVersion, cohort, percent, fingerprint, or dateTime.");
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
for (const key of Object.keys(raw)) if (!fields.includes(key)) issue(`${rulePath}.${key}`, `is not a field of a ${type} rule.`);
|
|
1811
|
+
switch (type) {
|
|
1812
|
+
case "platform": {
|
|
1813
|
+
const platforms = textList(raw.platforms, `${rulePath}.platforms`, issue, (text) => PLATFORMS.has(text), "must be \"ios\" or \"android\".");
|
|
1814
|
+
rules.push({
|
|
1815
|
+
type,
|
|
1816
|
+
platforms
|
|
1817
|
+
});
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
case "channel": {
|
|
1821
|
+
const channels = textList(raw.channels, `${rulePath}.channels`, issue, (text) => visibleText(text, MAX_RULE_TEXT_LENGTH), `must be a channel name of 1-${MAX_RULE_TEXT_LENGTH} characters.`);
|
|
1822
|
+
rules.push({
|
|
1823
|
+
type,
|
|
1824
|
+
channels
|
|
1825
|
+
});
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
case "appVersion": {
|
|
1829
|
+
const range = raw.range;
|
|
1830
|
+
if (!visibleText(range, MAX_RULE_TEXT_LENGTH) || !isValidRange(range.trim())) {
|
|
1831
|
+
issue(`${rulePath}.range`, "must be a semver range, such as >=1.4.0 or 2.x.");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
rules.push({
|
|
1835
|
+
type,
|
|
1836
|
+
range: range.trim()
|
|
1837
|
+
});
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
case "cohort": {
|
|
1841
|
+
const cohorts = textList(raw.cohorts, `${rulePath}.cohorts`, issue, isValidCohort, "must be a cohort from 1 to 1000, or a lowercase slug.").map(normalizeCohortValue);
|
|
1842
|
+
rules.push({
|
|
1843
|
+
type,
|
|
1844
|
+
cohorts: [...new Set(cohorts)]
|
|
1845
|
+
});
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
case "percent": {
|
|
1849
|
+
const { seed, from, to } = raw;
|
|
1850
|
+
if (typeof seed !== "string" || !SEED_PATTERN.test(seed)) {
|
|
1851
|
+
issue(`${rulePath}.seed`, "must be 1-64 letters, digits, _, and -.");
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const isPercent = (value) => typeof value === "number" && value >= 0 && value <= 100 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
|
|
1855
|
+
if (!isPercent(from) || !isPercent(to) || from >= to) {
|
|
1856
|
+
issue(rulePath, "takes from < to, each a percentage from 0 to 100 in steps of 0.1.");
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
rules.push({
|
|
1860
|
+
type,
|
|
1861
|
+
seed,
|
|
1862
|
+
from,
|
|
1863
|
+
to
|
|
1864
|
+
});
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
case "fingerprint": {
|
|
1868
|
+
const hashes = textList(raw.hashes, `${rulePath}.hashes`, issue, (text) => visibleText(text, MAX_RULE_TEXT_LENGTH), `must be a fingerprint of 1-${MAX_RULE_TEXT_LENGTH} characters.`);
|
|
1869
|
+
rules.push({
|
|
1870
|
+
type,
|
|
1871
|
+
hashes
|
|
1872
|
+
});
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
case "dateTime": {
|
|
1876
|
+
const instant = (field) => {
|
|
1877
|
+
const value = raw[field];
|
|
1878
|
+
if (value === void 0) return void 0;
|
|
1879
|
+
const ms = typeof value === "string" ? Date.parse(value) : NaN;
|
|
1880
|
+
if (typeof value !== "string" || !DATE_TIME_PATTERN.test(value) || !Number.isFinite(ms)) {
|
|
1881
|
+
issue(`${rulePath}.${field}`, "must be an ISO 8601 date-time with a time zone, such as 2026-10-08T10:00:00Z.");
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
return new Date(ms).toISOString();
|
|
1885
|
+
};
|
|
1886
|
+
const from = instant("from");
|
|
1887
|
+
const to = instant("to");
|
|
1888
|
+
if (from === null || to === null) return;
|
|
1889
|
+
if (from === void 0 && to === void 0) {
|
|
1890
|
+
issue(rulePath, "takes from, to, or both.");
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (from !== void 0 && to !== void 0 && from >= to) {
|
|
1894
|
+
issue(rulePath, "takes a from before its to.");
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
rules.push({
|
|
1898
|
+
type,
|
|
1899
|
+
...from === void 0 ? {} : { from },
|
|
1900
|
+
...to === void 0 ? {} : { to }
|
|
1901
|
+
});
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
return rules;
|
|
1907
|
+
};
|
|
1908
|
+
/** Whether one rule matches a device. */
|
|
1909
|
+
var matchesRemoteConfigRule = (rule, context) => {
|
|
1910
|
+
switch (rule.type) {
|
|
1911
|
+
case "platform": return context.platform !== void 0 && context.platform !== null && rule.platforms.includes(context.platform);
|
|
1912
|
+
case "channel": return typeof context.channel === "string" && rule.channels.includes(context.channel);
|
|
1913
|
+
case "appVersion": return typeof context.appVersion === "string" && semverSatisfies(rule.range, context.appVersion);
|
|
1914
|
+
case "cohort": return typeof context.cohort === "string" && rule.cohorts.includes(normalizeCohortValue(context.cohort));
|
|
1915
|
+
case "percent": {
|
|
1916
|
+
if (typeof context.cohort !== "string") return false;
|
|
1917
|
+
const cohort = getNumericCohortValue(context.cohort);
|
|
1918
|
+
if (cohort === null) return false;
|
|
1919
|
+
const position = getNumericCohortRolloutPosition(rule.seed, cohort);
|
|
1920
|
+
return position >= Math.round(rule.from * 10) && position < Math.round(rule.to * 10);
|
|
1921
|
+
}
|
|
1922
|
+
case "fingerprint": return typeof context.fingerprintHash === "string" && rule.hashes.includes(context.fingerprintHash);
|
|
1923
|
+
case "dateTime": return typeof context.now === "number" && (rule.from === void 0 || context.now >= Date.parse(rule.from)) && (rule.to === void 0 || context.now < Date.parse(rule.to));
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
/** Whether every rule of a condition matches a device. */
|
|
1927
|
+
var matchesRemoteConfigCondition = (condition, context) => condition.rules.every((rule) => matchesRemoteConfigRule(rule, context));
|
|
1928
|
+
/**
|
|
1929
|
+
* Every parameter's value for one device: the value of the first condition,
|
|
1930
|
+
* in the template's order, that matches and that the parameter has a value
|
|
1931
|
+
* for; otherwise its default value.
|
|
1932
|
+
*/
|
|
1933
|
+
var evaluateRemoteConfig = (template, context) => {
|
|
1934
|
+
const matched = template.conditions.filter((condition) => matchesRemoteConfigCondition(condition, context)).map(({ name }) => name);
|
|
1935
|
+
return Object.fromEntries(Object.entries(template.parameters).map(([key, parameter]) => {
|
|
1936
|
+
let condition = null;
|
|
1937
|
+
let chosen = parameter.defaultValue;
|
|
1938
|
+
for (const name of matched) {
|
|
1939
|
+
const value = ownEntry(parameter.conditionalValues, name);
|
|
1940
|
+
if (value !== void 0) {
|
|
1941
|
+
condition = name;
|
|
1942
|
+
chosen = value;
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return [key, {
|
|
1947
|
+
value: "value" in chosen ? chosen.value : null,
|
|
1948
|
+
condition
|
|
1949
|
+
}];
|
|
1950
|
+
}));
|
|
1951
|
+
};
|
|
1952
|
+
defineTable({
|
|
1953
|
+
id: {
|
|
1954
|
+
type: "string",
|
|
1955
|
+
maxLength: 16
|
|
1956
|
+
},
|
|
1957
|
+
version: { type: "integer" },
|
|
1958
|
+
template: { type: "json" },
|
|
1959
|
+
updated_at_ms: { type: "integer" }
|
|
1960
|
+
}, { key: ["id"] }), defineTable({
|
|
1961
|
+
version: { type: "integer" },
|
|
1962
|
+
template: { type: "json" },
|
|
1963
|
+
description: {
|
|
1964
|
+
type: "string",
|
|
1965
|
+
maxLength: 256,
|
|
1966
|
+
required: false
|
|
1967
|
+
},
|
|
1968
|
+
update_type: {
|
|
1969
|
+
type: "string",
|
|
1970
|
+
maxLength: 16
|
|
1971
|
+
},
|
|
1972
|
+
rollback_source: {
|
|
1973
|
+
type: "integer",
|
|
1974
|
+
required: false
|
|
1975
|
+
},
|
|
1976
|
+
created_at_ms: { type: "integer" }
|
|
1977
|
+
}, {
|
|
1978
|
+
key: ["version"],
|
|
1979
|
+
indexes: { byVersion: {
|
|
1980
|
+
eq: [],
|
|
1981
|
+
sort: ["version"]
|
|
1982
|
+
} }
|
|
1983
|
+
});
|
|
1984
|
+
var readBody = async (response) => response.json().catch(() => null);
|
|
1985
|
+
var field = (body, name) => typeof body === "object" && body !== null ? Reflect.get(body, name) : void 0;
|
|
1986
|
+
/** A published version's record, as a publish or a rollback answers it. */
|
|
1987
|
+
var isPublishedVersion = (body) => {
|
|
1988
|
+
const version = field(body, "version");
|
|
1989
|
+
const updateType = field(body, "updateType");
|
|
1990
|
+
return typeof version === "number" && Number.isSafeInteger(version) && version > 0 && (updateType === "PUBLISH" || updateType === "ROLLBACK") && typeof field(body, "createdAtMs") === "number";
|
|
1991
|
+
};
|
|
1992
|
+
var errorText = (body) => {
|
|
1993
|
+
const error = field(body, "error");
|
|
1994
|
+
return typeof error === "string" ? error : void 0;
|
|
1995
|
+
};
|
|
1996
|
+
/**
|
|
1997
|
+
* Remote Config over a self-hosted server's admin routes, for tooling that
|
|
1998
|
+
* reaches it through `standaloneRepository`. It answers as the API does: an
|
|
1999
|
+
* invalid template throws `RemoteConfigValidationError` with the server's
|
|
2000
|
+
* issues.
|
|
2001
|
+
*/
|
|
2002
|
+
var createRemoteConfigAdminApi = (fetchAdmin, { unavailable = () => /* @__PURE__ */ new Error("The server runs without remoteConfig(): its admin API serves no Remote Config routes.") } = {}) => {
|
|
2003
|
+
const request = async (path, init) => {
|
|
2004
|
+
const response = await fetchAdmin(path, init === void 0 ? void 0 : {
|
|
2005
|
+
method: init.method,
|
|
2006
|
+
headers: { "content-type": "application/json" },
|
|
2007
|
+
body: JSON.stringify(init.body)
|
|
2008
|
+
});
|
|
2009
|
+
const body = await readBody(response);
|
|
2010
|
+
if (response.status === 404 && errorText(body) === "Not found") throw unavailable();
|
|
2011
|
+
return {
|
|
2012
|
+
status: response.status,
|
|
2013
|
+
body
|
|
2014
|
+
};
|
|
2015
|
+
};
|
|
2016
|
+
const expectOk = (path, { status, body }) => {
|
|
2017
|
+
if (status >= 200 && status < 300) return body;
|
|
2018
|
+
throw new Error(errorText(body) ?? `The server answered ${path.split("?")[0]} with ${status}.`);
|
|
2019
|
+
};
|
|
2020
|
+
const published = (path, result) => {
|
|
2021
|
+
const currentVersion = field(result.body, "currentVersion");
|
|
2022
|
+
if (result.status === 409 && typeof currentVersion === "number" && Number.isSafeInteger(currentVersion)) return {
|
|
2023
|
+
status: "conflict",
|
|
2024
|
+
currentVersion
|
|
2025
|
+
};
|
|
2026
|
+
const issues = field(result.body, "issues");
|
|
2027
|
+
if (result.status === 400 && Array.isArray(issues)) throw new RemoteConfigValidationError(issues);
|
|
2028
|
+
const version = expectOk(path, result);
|
|
2029
|
+
if (!isPublishedVersion(version)) throw new Error(`The server answered ${path} without the version it published.`);
|
|
2030
|
+
return {
|
|
2031
|
+
status: "published",
|
|
2032
|
+
version
|
|
2033
|
+
};
|
|
2034
|
+
};
|
|
2035
|
+
return {
|
|
2036
|
+
getActive: async () => {
|
|
2037
|
+
const path = "/remote-config/template";
|
|
2038
|
+
return expectOk(path, await request(path));
|
|
2039
|
+
},
|
|
2040
|
+
listVersions: async ({ limit, cursor } = {}) => {
|
|
2041
|
+
const query = new URLSearchParams();
|
|
2042
|
+
if (limit !== void 0) query.set("limit", String(limit));
|
|
2043
|
+
if (cursor !== void 0) query.set("cursor", cursor);
|
|
2044
|
+
const path = `/remote-config/versions${query.size === 0 ? "" : `?${query}`}`;
|
|
2045
|
+
return expectOk(path, await request(path));
|
|
2046
|
+
},
|
|
2047
|
+
getVersion: async (version) => {
|
|
2048
|
+
const path = `/remote-config/versions/${version}`;
|
|
2049
|
+
const result = await request(path);
|
|
2050
|
+
return result.status === 404 ? null : expectOk(path, result);
|
|
2051
|
+
},
|
|
2052
|
+
publish: async ({ template, baseVersion, description }) => {
|
|
2053
|
+
const path = "/remote-config/template";
|
|
2054
|
+
return published(path, await request(path, {
|
|
2055
|
+
method: "PUT",
|
|
2056
|
+
body: {
|
|
2057
|
+
template,
|
|
2058
|
+
baseVersion,
|
|
2059
|
+
description
|
|
2060
|
+
}
|
|
2061
|
+
}));
|
|
2062
|
+
},
|
|
2063
|
+
rollback: async ({ version, baseVersion, description }) => {
|
|
2064
|
+
const path = `/remote-config/versions/${version}/rollback`;
|
|
2065
|
+
const result = await request(path, {
|
|
2066
|
+
method: "POST",
|
|
2067
|
+
body: {
|
|
2068
|
+
baseVersion,
|
|
2069
|
+
description
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
return result.status === 404 ? { status: "not_found" } : published(path, result);
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
};
|
|
2076
|
+
var plugins_exports = /* @__PURE__ */ __exportAll({
|
|
2077
|
+
API_KEY_HEADER_NAME: () => API_KEY_HEADER_NAME,
|
|
2078
|
+
REMOTE_CONFIG_MAX_CONDITIONS: () => 500,
|
|
2079
|
+
REMOTE_CONFIG_MAX_PARAMETERS: () => REMOTE_CONFIG_MAX_PARAMETERS,
|
|
2080
|
+
REMOTE_CONFIG_MAX_TEMPLATE_LENGTH: () => REMOTE_CONFIG_MAX_TEMPLATE_LENGTH,
|
|
2081
|
+
RemoteConfigValidationError: () => RemoteConfigValidationError,
|
|
2082
|
+
evaluateRemoteConfig: () => evaluateRemoteConfig,
|
|
2083
|
+
validateRemoteConfigTemplate: () => validateRemoteConfigTemplate
|
|
2084
|
+
});
|
|
2085
|
+
//#endregion
|
|
2086
|
+
export { createInsightsReads as a, createInsightsAdminReads as i, createRemoteConfigAdminApi as n, createInsightsModel as o, RemoteConfigValidationError as r, plugins_exports as t };
|