@nextcommerce/campaigns-os 1.33.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/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { invocationPrefixFor } from "./install-mode.mjs";
|
|
3
|
+
import { dirname as installModeDirname, resolve as installModeResolve } from "node:path";
|
|
4
|
+
import { fileURLToPath as installModeFileUrl } from "node:url";
|
|
5
|
+
const PACKAGE_ROOT = installModeResolve(installModeDirname(installModeFileUrl(import.meta.url)), "..");
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
captureOrigin,
|
|
9
|
+
captureProblemRecord,
|
|
10
|
+
MAX_PAGE_LOAD_MEDIA_ANCESTORS,
|
|
11
|
+
MAX_PAGE_LOAD_MEDIA_ELEMENTS,
|
|
12
|
+
MAX_PAGE_LOAD_MEDIA_SOURCES_PER_ELEMENT,
|
|
13
|
+
MAX_PAGE_LOAD_RESOURCE_LEDGER_ENTRIES,
|
|
14
|
+
MAX_PAGE_LOAD_RESPONSE_RECORDS,
|
|
15
|
+
MAX_POLISH_CAPTURE_URL_LENGTH,
|
|
16
|
+
redirectChainRecord,
|
|
17
|
+
singleResponseRecord,
|
|
18
|
+
} from "./polish-capture.mjs";
|
|
19
|
+
import { launchPackageChromium, PLAYWRIGHT_INSTALL_HINT } from "./browser-launch.mjs";
|
|
20
|
+
import {
|
|
21
|
+
boundedPolishDeadline,
|
|
22
|
+
POLISH_BROWSER_CELL_DEADLINE_MS,
|
|
23
|
+
POLISH_BROWSER_UNAVAILABLE_ERROR_CODE,
|
|
24
|
+
POLISH_BROWSER_CLEANUP_DEADLINE_MS,
|
|
25
|
+
POLISH_BROWSER_STARTUP_DEADLINE_MS,
|
|
26
|
+
POLISH_PRODUCER_CLEANUP_ERROR_CODE,
|
|
27
|
+
POLISH_PRODUCER_TIMEOUT_ERROR_CODE,
|
|
28
|
+
polishProducerCleanupError,
|
|
29
|
+
polishProducerTimeoutError,
|
|
30
|
+
runWithPolishProducerDeadline,
|
|
31
|
+
} from "./polish-deadline.mjs";
|
|
32
|
+
|
|
33
|
+
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
34
|
+
const NETWORKIDLE_TIMEOUT_MS = 5_000;
|
|
35
|
+
|
|
36
|
+
const NETWORK_EVENTS = Object.freeze([
|
|
37
|
+
"Network.requestWillBeSent",
|
|
38
|
+
"Network.responseReceived",
|
|
39
|
+
"Network.loadingFinished",
|
|
40
|
+
"Network.loadingFailed",
|
|
41
|
+
"Network.dataReceived",
|
|
42
|
+
"Network.requestServedFromCache",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const AUTH_COOKIE_ERROR = "Campaigns OS polish capture received a malformed or empty --auth-cookie value.";
|
|
46
|
+
const COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
47
|
+
function browserUnavailableError(message) {
|
|
48
|
+
const error = new Error(message);
|
|
49
|
+
error.code = POLISH_BROWSER_UNAVAILABLE_ERROR_CODE;
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nonnegativeInteger(value) {
|
|
54
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function uniqueStrings(values) {
|
|
58
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value !== ""))];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A non-http(s) URL (data:, blob:, about:, an extension scheme) is kept as
|
|
62
|
+
// its scheme alone: the payload of a data: URL is page content, not a
|
|
63
|
+
// network address, and bounding it by length would misreport a long inline
|
|
64
|
+
// image as an overflow instead of what it is.
|
|
65
|
+
const URL_SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
|
|
66
|
+
|
|
67
|
+
function boundedCaptureUrl(value) {
|
|
68
|
+
if (typeof value !== "string") return null;
|
|
69
|
+
const scheme = URL_SCHEME_PATTERN.exec(value)?.[1]?.toLowerCase();
|
|
70
|
+
if (scheme && scheme !== "http" && scheme !== "https") return `${scheme}:`;
|
|
71
|
+
return value.length > MAX_POLISH_CAPTURE_URL_LENGTH ? "[url-too-long]" : value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function responseHeader(response, name) {
|
|
75
|
+
if (!response?.headers || typeof response.headers !== "object" || Array.isArray(response.headers)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const wanted = name.toLowerCase();
|
|
79
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
80
|
+
if (key.toLowerCase() === wanted) return value;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function decimalHeaderInteger(value) {
|
|
86
|
+
const token = typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "";
|
|
87
|
+
if (!/^\d+$/.test(token)) return undefined;
|
|
88
|
+
const parsed = Number(token);
|
|
89
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function declaredResponseLength(response) {
|
|
93
|
+
const contentRange = responseHeader(response, "content-range");
|
|
94
|
+
if (typeof contentRange === "string") {
|
|
95
|
+
const match = /^bytes\s+\d+\s*-\s*\d+\s*\/\s*(\d+)\s*$/i.exec(contentRange.trim());
|
|
96
|
+
const total = decimalHeaderInteger(match?.[1]);
|
|
97
|
+
if (total !== undefined) return total;
|
|
98
|
+
}
|
|
99
|
+
return decimalHeaderInteger(responseHeader(response, "content-length"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function projectProtocolResponse(value) {
|
|
103
|
+
if (!value || typeof value !== "object") return null;
|
|
104
|
+
const declaredDataLength = declaredResponseLength(value);
|
|
105
|
+
return {
|
|
106
|
+
...(typeof value.url === "string" ? { url: boundedCaptureUrl(value.url) } : {}),
|
|
107
|
+
...(Number.isInteger(value.status) ? { status: value.status } : {}),
|
|
108
|
+
...(typeof value.mimeType === "string" ? { mimeType: value.mimeType } : {}),
|
|
109
|
+
...(nonnegativeInteger(value.encodedDataLength) !== undefined
|
|
110
|
+
? { encodedDataLength: value.encodedDataLength }
|
|
111
|
+
: {}),
|
|
112
|
+
...(declaredDataLength !== undefined ? { declaredDataLength } : {}),
|
|
113
|
+
fromDiskCache: Boolean(value.fromDiskCache),
|
|
114
|
+
fromPrefetchCache: Boolean(value.fromPrefetchCache),
|
|
115
|
+
fromServiceWorker: Boolean(value.fromServiceWorker),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseAuthCookie(value) {
|
|
120
|
+
if (value === null || value === undefined) return [];
|
|
121
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(AUTH_COOKIE_ERROR);
|
|
122
|
+
const cookies = [];
|
|
123
|
+
const names = new Set();
|
|
124
|
+
for (const rawPair of value.split(";")) {
|
|
125
|
+
const pair = rawPair.trim();
|
|
126
|
+
const separator = pair.indexOf("=");
|
|
127
|
+
if (!pair || separator <= 0) throw new Error(AUTH_COOKIE_ERROR);
|
|
128
|
+
const name = pair.slice(0, separator).trim();
|
|
129
|
+
const cookieValue = pair.slice(separator + 1).trim();
|
|
130
|
+
if (!COOKIE_NAME_PATTERN.test(name)
|
|
131
|
+
|| /[\u0000-\u001f\u007f;]/.test(cookieValue)
|
|
132
|
+
|| names.has(name)) {
|
|
133
|
+
throw new Error(AUTH_COOKIE_ERROR);
|
|
134
|
+
}
|
|
135
|
+
names.add(name);
|
|
136
|
+
cookies.push({ name, value: cookieValue });
|
|
137
|
+
}
|
|
138
|
+
return cookies;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function observedResourceType(event = {}) {
|
|
142
|
+
const type = typeof event?.type === "string" ? event.type : null;
|
|
143
|
+
// Newer CDP protocol versions report the CORS preflight leg as "Preflight";
|
|
144
|
+
// older ones report it as "Other". The request method identifies it either
|
|
145
|
+
// way, so classify at the source — aggregation cannot tell an older-protocol
|
|
146
|
+
// preflight apart from a genuine "Other" request sharing the URL.
|
|
147
|
+
if (type === "Other" && event?.request?.method === "OPTIONS") return "Preflight";
|
|
148
|
+
return type;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function currentRequest(event = {}) {
|
|
152
|
+
return {
|
|
153
|
+
requestUrl: boundedCaptureUrl(event?.request?.url),
|
|
154
|
+
resourceType: observedResourceType(event),
|
|
155
|
+
response: null,
|
|
156
|
+
requestServedFromCache: false,
|
|
157
|
+
frameId: typeof event?.frameId === "string" ? event.frameId : null,
|
|
158
|
+
loaderId: typeof event?.loaderId === "string" ? event.loaderId : null,
|
|
159
|
+
encodedDataLengthLowerBound: 0,
|
|
160
|
+
encodedDataObserved: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function responseRecord(current, {
|
|
165
|
+
response = current?.response,
|
|
166
|
+
encodedDataLength,
|
|
167
|
+
failed = false,
|
|
168
|
+
canceled = false,
|
|
169
|
+
} = {}) {
|
|
170
|
+
const url = typeof response?.url === "string" && response.url !== ""
|
|
171
|
+
? response.url
|
|
172
|
+
: current?.requestUrl;
|
|
173
|
+
const sourceUrls = uniqueStrings([current?.requestUrl]);
|
|
174
|
+
const measuredLength = nonnegativeInteger(encodedDataLength);
|
|
175
|
+
const lowerBound = current?.encodedDataObserved
|
|
176
|
+
? nonnegativeInteger(current.encodedDataLengthLowerBound)
|
|
177
|
+
: undefined;
|
|
178
|
+
const declaredLength = canceled ? nonnegativeInteger(response?.declaredDataLength) : undefined;
|
|
179
|
+
const retainedLength = measuredLength === undefined
|
|
180
|
+
? lowerBound
|
|
181
|
+
: lowerBound === undefined ? measuredLength : Math.max(measuredLength, lowerBound);
|
|
182
|
+
return {
|
|
183
|
+
...(typeof url === "string" && url !== "" ? { url } : {}),
|
|
184
|
+
...(typeof current?.resourceType === "string" && current.resourceType !== ""
|
|
185
|
+
? { resource_type: current.resourceType }
|
|
186
|
+
: {}),
|
|
187
|
+
...(Number.isInteger(response?.status) ? { status: response.status } : {}),
|
|
188
|
+
...(typeof response?.mimeType === "string" && response.mimeType !== ""
|
|
189
|
+
? { mime_type: response.mimeType }
|
|
190
|
+
: {}),
|
|
191
|
+
...(retainedLength !== undefined
|
|
192
|
+
? { encoded_data_length: retainedLength }
|
|
193
|
+
: {}),
|
|
194
|
+
...(declaredLength !== undefined ? { declared_data_length: declaredLength } : {}),
|
|
195
|
+
...(canceled ? { canceled: true } : {}),
|
|
196
|
+
source_urls: sourceUrls,
|
|
197
|
+
from_disk_cache: Boolean(response?.fromDiskCache),
|
|
198
|
+
from_prefetch_cache: Boolean(response?.fromPrefetchCache),
|
|
199
|
+
from_service_worker: Boolean(response?.fromServiceWorker),
|
|
200
|
+
request_served_from_cache: Boolean(current?.requestServedFromCache),
|
|
201
|
+
failed: Boolean(failed),
|
|
202
|
+
__frame_id: current?.frameId || null,
|
|
203
|
+
__loader_id: current?.loaderId || null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function attributableResponseRecord(record) {
|
|
208
|
+
return typeof record?.url === "string" && record.url !== ""
|
|
209
|
+
&& typeof record?.resource_type === "string" && record.resource_type !== "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function completeResponseRecord(record) {
|
|
213
|
+
return typeof record?.url === "string"
|
|
214
|
+
&& typeof record?.resource_type === "string"
|
|
215
|
+
&& Number.isInteger(record?.status)
|
|
216
|
+
&& (nonnegativeInteger(record?.encoded_data_length) !== undefined
|
|
217
|
+
|| (record?.canceled === true && nonnegativeInteger(record?.declared_data_length) !== undefined));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createNetworkCollector() {
|
|
221
|
+
const requests = new Map();
|
|
222
|
+
let collectionFailed = false;
|
|
223
|
+
let responseRecordCount = 0;
|
|
224
|
+
let responseOverflow = false;
|
|
225
|
+
|
|
226
|
+
function stateFor(requestId) {
|
|
227
|
+
if (typeof requestId !== "string" || requestId === "") {
|
|
228
|
+
collectionFailed = true;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
let state = requests.get(requestId);
|
|
232
|
+
if (!state) {
|
|
233
|
+
if (requests.size >= MAX_PAGE_LOAD_RESOURCE_LEDGER_ENTRIES) {
|
|
234
|
+
responseOverflow = true;
|
|
235
|
+
collectionFailed = true;
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
state = { requestId, hops: [], current: null, redirected: false };
|
|
239
|
+
requests.set(requestId, state);
|
|
240
|
+
}
|
|
241
|
+
return state;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function finishCurrent(state, options = {}) {
|
|
245
|
+
if (!state?.current) {
|
|
246
|
+
// A canceled terminal event with nothing in flight is either a
|
|
247
|
+
// duplicate after the load already finished (dropped is only consulted
|
|
248
|
+
// for zero-hop states, so the emitted record is unaffected) or an abort
|
|
249
|
+
// that raced listener attachment; neither is a measurement failure.
|
|
250
|
+
if (options.canceled) {
|
|
251
|
+
if (state) state.dropped = true;
|
|
252
|
+
} else {
|
|
253
|
+
collectionFailed = true;
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const record = responseRecord(state.current, options);
|
|
258
|
+
if (options.canceled) {
|
|
259
|
+
// Browser-canceled loads (aborted media range requests, fetches cut off
|
|
260
|
+
// by navigation) are normal page behavior, not measurement failures.
|
|
261
|
+
// Keep complete records as observed responses; drop incomplete ones
|
|
262
|
+
// without failing the collection.
|
|
263
|
+
if (!completeResponseRecord(record)) {
|
|
264
|
+
state.dropped = true;
|
|
265
|
+
} else if (responseRecordCount >= MAX_PAGE_LOAD_RESPONSE_RECORDS) {
|
|
266
|
+
// Unlike the non-canceled cap path this does not fail the collection:
|
|
267
|
+
// the drop is normal page behavior, but the overflow sentinel still
|
|
268
|
+
// projects a problem downstream so the loss is never silent.
|
|
269
|
+
responseOverflow = true;
|
|
270
|
+
state.dropped = true;
|
|
271
|
+
} else {
|
|
272
|
+
state.hops.push(record);
|
|
273
|
+
responseRecordCount += 1;
|
|
274
|
+
}
|
|
275
|
+
state.current = null;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// A failed load is recorded as failed and left to capture attribution
|
|
279
|
+
// (polish-capture.mjs decides whether the failure voids the collection by
|
|
280
|
+
// origin and role). The collection itself fails only when the record
|
|
281
|
+
// cannot be attributed — no URL or resource type to attribute it by — or
|
|
282
|
+
// when a non-failed load is missing its terminal measurement.
|
|
283
|
+
if (record.failed ? !attributableResponseRecord(record) : !completeResponseRecord(record)) {
|
|
284
|
+
collectionFailed = true;
|
|
285
|
+
}
|
|
286
|
+
if (responseRecordCount >= MAX_PAGE_LOAD_RESPONSE_RECORDS) {
|
|
287
|
+
responseOverflow = true;
|
|
288
|
+
collectionFailed = true;
|
|
289
|
+
} else {
|
|
290
|
+
state.hops.push(record);
|
|
291
|
+
responseRecordCount += 1;
|
|
292
|
+
}
|
|
293
|
+
state.current = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const handlers = {
|
|
297
|
+
"Network.requestWillBeSent"(event = {}) {
|
|
298
|
+
const state = stateFor(event.requestId);
|
|
299
|
+
if (!state) return;
|
|
300
|
+
if (event.redirectResponse) {
|
|
301
|
+
state.redirected = true;
|
|
302
|
+
if (!state.current) {
|
|
303
|
+
collectionFailed = true;
|
|
304
|
+
state.current = {
|
|
305
|
+
requestUrl: boundedCaptureUrl(event.redirectResponse.url),
|
|
306
|
+
resourceType: observedResourceType(event),
|
|
307
|
+
response: null,
|
|
308
|
+
requestServedFromCache: false,
|
|
309
|
+
frameId: typeof event?.frameId === "string" ? event.frameId : null,
|
|
310
|
+
loaderId: typeof event?.loaderId === "string" ? event.loaderId : null,
|
|
311
|
+
encodedDataLengthLowerBound: 0,
|
|
312
|
+
encodedDataObserved: false,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
finishCurrent(state, {
|
|
316
|
+
response: projectProtocolResponse(event.redirectResponse),
|
|
317
|
+
encodedDataLength: event.redirectResponse.encodedDataLength,
|
|
318
|
+
});
|
|
319
|
+
} else if (state.current || state.hops.length > 0) {
|
|
320
|
+
collectionFailed = true;
|
|
321
|
+
if (state.current) finishCurrent(state, { failed: true });
|
|
322
|
+
}
|
|
323
|
+
state.current = currentRequest(event);
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
"Network.responseReceived"(event = {}) {
|
|
327
|
+
const state = stateFor(event.requestId);
|
|
328
|
+
if (!state) return;
|
|
329
|
+
if (!state.current) {
|
|
330
|
+
collectionFailed = true;
|
|
331
|
+
state.current = {
|
|
332
|
+
requestUrl: boundedCaptureUrl(event?.response?.url),
|
|
333
|
+
resourceType: typeof event.type === "string" ? event.type : null,
|
|
334
|
+
response: null,
|
|
335
|
+
requestServedFromCache: false,
|
|
336
|
+
frameId: typeof event?.frameId === "string" ? event.frameId : null,
|
|
337
|
+
loaderId: typeof event?.loaderId === "string" ? event.loaderId : null,
|
|
338
|
+
encodedDataLengthLowerBound: 0,
|
|
339
|
+
encodedDataObserved: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (state.current.response) collectionFailed = true;
|
|
343
|
+
state.current.response = projectProtocolResponse(event.response);
|
|
344
|
+
if (!state.current.resourceType && typeof event.type === "string") {
|
|
345
|
+
state.current.resourceType = event.type;
|
|
346
|
+
}
|
|
347
|
+
if (!state.current.frameId && typeof event.frameId === "string") state.current.frameId = event.frameId;
|
|
348
|
+
if (!state.current.loaderId && typeof event.loaderId === "string") state.current.loaderId = event.loaderId;
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
"Network.loadingFinished"(event = {}) {
|
|
352
|
+
const state = stateFor(event.requestId);
|
|
353
|
+
if (!state) return;
|
|
354
|
+
finishCurrent(state, { encodedDataLength: event.encodedDataLength });
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
"Network.loadingFailed"(event = {}) {
|
|
358
|
+
const state = stateFor(event.requestId);
|
|
359
|
+
if (!state) return;
|
|
360
|
+
const canceled = event.canceled === true
|
|
361
|
+
|| event.errorText === "net::ERR_ABORTED";
|
|
362
|
+
if (canceled) {
|
|
363
|
+
finishCurrent(state, { failed: false, canceled: true });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
finishCurrent(state, { failed: true });
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
"Network.dataReceived"(event = {}) {
|
|
370
|
+
const state = stateFor(event.requestId);
|
|
371
|
+
if (!state?.current) {
|
|
372
|
+
collectionFailed = true;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const encodedDataLength = nonnegativeInteger(event.encodedDataLength);
|
|
376
|
+
if (encodedDataLength === undefined) {
|
|
377
|
+
collectionFailed = true;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
state.current.encodedDataObserved = true;
|
|
381
|
+
const next = state.current.encodedDataLengthLowerBound + encodedDataLength;
|
|
382
|
+
if (!Number.isSafeInteger(next)) {
|
|
383
|
+
state.current.encodedDataLengthLowerBound = Number.MAX_SAFE_INTEGER;
|
|
384
|
+
collectionFailed = true;
|
|
385
|
+
} else {
|
|
386
|
+
state.current.encodedDataLengthLowerBound = next;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
"Network.requestServedFromCache"(event = {}) {
|
|
391
|
+
const state = stateFor(event.requestId);
|
|
392
|
+
if (!state) return;
|
|
393
|
+
if (!state.current) {
|
|
394
|
+
collectionFailed = true;
|
|
395
|
+
state.current = {
|
|
396
|
+
requestUrl: null,
|
|
397
|
+
resourceType: null,
|
|
398
|
+
response: null,
|
|
399
|
+
requestServedFromCache: false,
|
|
400
|
+
frameId: null,
|
|
401
|
+
loaderId: null,
|
|
402
|
+
encodedDataLengthLowerBound: 0,
|
|
403
|
+
encodedDataObserved: false,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
state.current.requestServedFromCache = true;
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
listen(session) {
|
|
412
|
+
for (const event of NETWORK_EVENTS) session.on(event, handlers[event]);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
finish({ mainFrameId, mainLoaderId, finalDocumentUrl } = {}) {
|
|
416
|
+
const responses = [];
|
|
417
|
+
const comparableFinalUrl = comparableDocumentUrl(finalDocumentUrl);
|
|
418
|
+
const contextFingerprint = documentContextFingerprint(mainFrameId, mainLoaderId);
|
|
419
|
+
const projectRecord = (record) => {
|
|
420
|
+
const { __frame_id: frameId, __loader_id: loaderId, ...projection } = record;
|
|
421
|
+
const finalMainDocument = projection.resource_type === "Document"
|
|
422
|
+
&& frameId === mainFrameId
|
|
423
|
+
&& loaderId === mainLoaderId
|
|
424
|
+
&& comparableDocumentUrl(projection.url) === comparableFinalUrl;
|
|
425
|
+
return finalMainDocument && contextFingerprint
|
|
426
|
+
? { ...projection, is_final_main_document: true, document_context_fingerprint: contextFingerprint }
|
|
427
|
+
: projection;
|
|
428
|
+
};
|
|
429
|
+
for (const state of requests.values()) {
|
|
430
|
+
// A request still in flight when the capture window closes (an
|
|
431
|
+
// autoplaying video stream, a long-poll) is cut off by the capture
|
|
432
|
+
// itself — account for it like a browser-canceled load, not a failure.
|
|
433
|
+
if (state.current) finishCurrent(state, { failed: false, canceled: true });
|
|
434
|
+
if (state.redirected || state.hops.length > 1) {
|
|
435
|
+
responses.push(redirectChainRecord(state.requestId, state.hops.map(projectRecord)));
|
|
436
|
+
} else if (state.hops.length === 1) {
|
|
437
|
+
responses.push(singleResponseRecord(state.requestId, projectRecord(state.hops[0])));
|
|
438
|
+
} else if (!state.dropped) {
|
|
439
|
+
collectionFailed = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (responseOverflow) responses.push(captureProblemRecord("response_record_overflow"));
|
|
443
|
+
return {
|
|
444
|
+
responseCollectionStatus: collectionFailed ? "failed" : "complete",
|
|
445
|
+
responses,
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function collectMediaElements(page) {
|
|
452
|
+
return page.evaluate((limits) => {
|
|
453
|
+
const registryKey = "__campaigns_os_polish_media_registry_v1__";
|
|
454
|
+
let registry = document[registryKey];
|
|
455
|
+
if (!registry) {
|
|
456
|
+
registry = { ids: new WeakMap(), nextId: 0 };
|
|
457
|
+
Object.defineProperty(document, registryKey, { configurable: true, value: registry });
|
|
458
|
+
}
|
|
459
|
+
const captureId = (element) => {
|
|
460
|
+
let id = registry.ids.get(element);
|
|
461
|
+
if (!id) {
|
|
462
|
+
id = `media-${registry.nextId}`;
|
|
463
|
+
registry.nextId += 1;
|
|
464
|
+
registry.ids.set(element, id);
|
|
465
|
+
}
|
|
466
|
+
return id;
|
|
467
|
+
};
|
|
468
|
+
const styleProjection = (node) => {
|
|
469
|
+
const style = getComputedStyle(node);
|
|
470
|
+
return { display: style.display, visibility: style.visibility };
|
|
471
|
+
};
|
|
472
|
+
const boundedUrl = (value, overflow) => {
|
|
473
|
+
if (typeof value !== "string") return value;
|
|
474
|
+
if (value.length <= limits.urlLength) return value;
|
|
475
|
+
overflow.count += 1;
|
|
476
|
+
return "[url-too-long]";
|
|
477
|
+
};
|
|
478
|
+
const nodes = document.querySelectorAll("video, audio");
|
|
479
|
+
const elements = Array.prototype.slice.call(nodes, 0, limits.mediaElements).map((element) => {
|
|
480
|
+
const urlOverflow = { count: 0 };
|
|
481
|
+
const ancestorStyles = [];
|
|
482
|
+
let ancestor = element.parentElement;
|
|
483
|
+
while (ancestor && ancestorStyles.length < limits.ancestors) {
|
|
484
|
+
ancestorStyles.push(styleProjection(ancestor));
|
|
485
|
+
ancestor = ancestor.parentElement;
|
|
486
|
+
}
|
|
487
|
+
const sources = element.querySelectorAll("source");
|
|
488
|
+
const bounds = element.getBoundingClientRect();
|
|
489
|
+
return {
|
|
490
|
+
capture_element_id: captureId(element),
|
|
491
|
+
tag_name: element.tagName.toLowerCase(),
|
|
492
|
+
current_src: typeof element.currentSrc === "string" ? boundedUrl(element.currentSrc, urlOverflow) : null,
|
|
493
|
+
src_attribute: boundedUrl(element.getAttribute("src"), urlOverflow),
|
|
494
|
+
source_src_attributes: Array.prototype.slice.call(sources, 0, limits.sources)
|
|
495
|
+
.map((source) => boundedUrl(source.getAttribute("src") ?? "", urlOverflow)),
|
|
496
|
+
preload_attribute: element.getAttribute("preload"),
|
|
497
|
+
computed_style: styleProjection(element),
|
|
498
|
+
ancestor_styles: ancestorStyles,
|
|
499
|
+
bounding_box: { width: bounds.width, height: bounds.height },
|
|
500
|
+
source_overflow_count: Math.max(0, sources.length - limits.sources),
|
|
501
|
+
ancestor_overflow_count: ancestor ? 1 : 0,
|
|
502
|
+
url_overflow_count: urlOverflow.count,
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
return { observed_element_count: nodes.length, elements };
|
|
506
|
+
}, {
|
|
507
|
+
mediaElements: MAX_PAGE_LOAD_MEDIA_ELEMENTS,
|
|
508
|
+
sources: MAX_PAGE_LOAD_MEDIA_SOURCES_PER_ELEMENT,
|
|
509
|
+
ancestors: MAX_PAGE_LOAD_MEDIA_ANCESTORS,
|
|
510
|
+
urlLength: MAX_POLISH_CAPTURE_URL_LENGTH,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function preferredResolvedValue(finalValue, initialValue) {
|
|
515
|
+
return typeof finalValue === "string" && finalValue !== "" ? finalValue : initialValue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function observedMediaSources(...elements) {
|
|
519
|
+
return [...new Set(elements.flatMap((element) => [
|
|
520
|
+
element?.current_src,
|
|
521
|
+
element?.src_attribute,
|
|
522
|
+
...(Array.isArray(element?.source_src_attributes) ? element.source_src_attributes : []),
|
|
523
|
+
...(Array.isArray(element?.observed_source_urls) ? element.observed_source_urls : []),
|
|
524
|
+
]).filter((value) => typeof value === "string" && value !== ""))];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function mergeMediaElementSnapshots(initialSnapshot, finalSnapshot) {
|
|
528
|
+
const initialElements = Array.isArray(initialSnapshot?.elements) ? initialSnapshot.elements : [];
|
|
529
|
+
const finalElements = Array.isArray(finalSnapshot?.elements) ? finalSnapshot.elements : [];
|
|
530
|
+
const initialById = new Map(initialElements.map((element) => [element?.capture_element_id, element]));
|
|
531
|
+
const finalById = new Map(finalElements.map((element) => [element?.capture_element_id, element]));
|
|
532
|
+
if ([...initialById.keys(), ...finalById.keys()].some((id) => typeof id !== "string" || id === "")
|
|
533
|
+
|| initialById.size !== initialElements.length
|
|
534
|
+
|| finalById.size !== finalElements.length) {
|
|
535
|
+
throw new Error("Campaigns OS polish capture could not correlate bounded media snapshots.");
|
|
536
|
+
}
|
|
537
|
+
const ids = [...initialById.keys(), ...finalById.keys().filter((id) => !initialById.has(id))];
|
|
538
|
+
const merged = ids.slice(0, MAX_PAGE_LOAD_MEDIA_ELEMENTS).map((id) => {
|
|
539
|
+
const initial = initialById.get(id);
|
|
540
|
+
const final = finalById.get(id);
|
|
541
|
+
const atLoad = initial || final;
|
|
542
|
+
const resolved = final || initial;
|
|
543
|
+
const {
|
|
544
|
+
capture_element_id: ignoredId,
|
|
545
|
+
source_overflow_count: initialSourceOverflow,
|
|
546
|
+
ancestor_overflow_count: initialAncestorOverflow,
|
|
547
|
+
url_overflow_count: initialUrlOverflow,
|
|
548
|
+
...projection
|
|
549
|
+
} = atLoad;
|
|
550
|
+
const sourceHistory = observedMediaSources(atLoad, resolved);
|
|
551
|
+
const sourceOverflowCount = Math.max(initialSourceOverflow || 0, resolved?.source_overflow_count || 0)
|
|
552
|
+
+ Math.max(0, sourceHistory.length - MAX_PAGE_LOAD_MEDIA_SOURCES_PER_ELEMENT);
|
|
553
|
+
const ancestorOverflowCount = Math.max(initialAncestorOverflow || 0, resolved?.ancestor_overflow_count || 0);
|
|
554
|
+
const urlOverflowCount = Math.max(initialUrlOverflow || 0, resolved?.url_overflow_count || 0);
|
|
555
|
+
return {
|
|
556
|
+
...projection,
|
|
557
|
+
current_src: preferredResolvedValue(resolved?.current_src, atLoad?.current_src),
|
|
558
|
+
src_attribute: preferredResolvedValue(resolved?.src_attribute, atLoad?.src_attribute),
|
|
559
|
+
source_src_attributes: Array.isArray(resolved?.source_src_attributes)
|
|
560
|
+
&& resolved.source_src_attributes.length > 0
|
|
561
|
+
? resolved.source_src_attributes
|
|
562
|
+
: atLoad.source_src_attributes,
|
|
563
|
+
observed_source_urls: sourceHistory.slice(0, MAX_PAGE_LOAD_MEDIA_SOURCES_PER_ELEMENT),
|
|
564
|
+
...(sourceOverflowCount > 0 ? { source_overflow_count: sourceOverflowCount } : {}),
|
|
565
|
+
...(ancestorOverflowCount > 0 ? { ancestor_overflow_count: ancestorOverflowCount } : {}),
|
|
566
|
+
...(urlOverflowCount > 0 ? { url_overflow_count: urlOverflowCount } : {}),
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
const initialCount = nonnegativeInteger(initialSnapshot?.observed_element_count) || initialElements.length;
|
|
570
|
+
const finalCount = nonnegativeInteger(finalSnapshot?.observed_element_count) || finalElements.length;
|
|
571
|
+
const observedElementCount = Math.max(initialCount, finalCount, ids.length);
|
|
572
|
+
Object.defineProperty(merged, "observed_element_count", {
|
|
573
|
+
configurable: false,
|
|
574
|
+
enumerable: false,
|
|
575
|
+
value: observedElementCount,
|
|
576
|
+
});
|
|
577
|
+
return merged;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function comparableDocumentUrl(value) {
|
|
581
|
+
try {
|
|
582
|
+
const url = new URL(value);
|
|
583
|
+
url.hash = "";
|
|
584
|
+
return url.href;
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function documentContextFingerprint(frameId, loaderId) {
|
|
591
|
+
if (typeof frameId !== "string" || frameId === ""
|
|
592
|
+
|| typeof loaderId !== "string" || loaderId === "") return null;
|
|
593
|
+
return `sha256:${createHash("sha256").update(`${frameId}\u0000${loaderId}`).digest("hex")}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function timeoutError(error) {
|
|
597
|
+
return error?.name === "TimeoutError";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function durationSince(startedAt) {
|
|
601
|
+
return Math.max(0, Math.round(performance.now() - startedAt));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function drainProtocolEvents() {
|
|
605
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function polishBrowserMissing(kind, error) {
|
|
609
|
+
return browserUnavailableError(kind === "package"
|
|
610
|
+
? [
|
|
611
|
+
"Playwright is not installed for Campaigns OS polish capture.",
|
|
612
|
+
PLAYWRIGHT_INSTALL_HINT,
|
|
613
|
+
"Then rerun `campaigns-os polish capture`.",
|
|
614
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
615
|
+
].join(" ")
|
|
616
|
+
: [
|
|
617
|
+
"Playwright Chromium is not installed for Campaigns OS polish capture.",
|
|
618
|
+
installBrowserHint("polish"),
|
|
619
|
+
].join(" "));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export async function createPolishBrowserAdapter({
|
|
623
|
+
headed = false,
|
|
624
|
+
authCookie = null,
|
|
625
|
+
chromium: injectedChromium,
|
|
626
|
+
cellDeadlineMs,
|
|
627
|
+
cleanupDeadlineMs,
|
|
628
|
+
startupDeadlineMs,
|
|
629
|
+
} = {}) {
|
|
630
|
+
const authCookies = parseAuthCookie(authCookie);
|
|
631
|
+
const boundedCellDeadlineMs = boundedPolishDeadline(cellDeadlineMs, POLISH_BROWSER_CELL_DEADLINE_MS);
|
|
632
|
+
const boundedCleanupDeadlineMs = boundedPolishDeadline(
|
|
633
|
+
cleanupDeadlineMs,
|
|
634
|
+
POLISH_BROWSER_CLEANUP_DEADLINE_MS,
|
|
635
|
+
);
|
|
636
|
+
const boundedStartupDeadlineMs = boundedPolishDeadline(
|
|
637
|
+
startupDeadlineMs,
|
|
638
|
+
POLISH_BROWSER_STARTUP_DEADLINE_MS,
|
|
639
|
+
);
|
|
640
|
+
let browser;
|
|
641
|
+
let startupTimedOut = false;
|
|
642
|
+
const startupAbort = new AbortController();
|
|
643
|
+
const startupPromise = Promise.resolve().then(async () => {
|
|
644
|
+
let launchedBrowser;
|
|
645
|
+
try {
|
|
646
|
+
launchedBrowser = await launchPackageChromium({
|
|
647
|
+
headed,
|
|
648
|
+
chromium: injectedChromium,
|
|
649
|
+
onMissing: polishBrowserMissing,
|
|
650
|
+
signal: startupAbort.signal,
|
|
651
|
+
});
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (startupTimedOut) throw polishProducerTimeoutError();
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
if (startupTimedOut) {
|
|
657
|
+
if (typeof launchedBrowser?.close === "function") {
|
|
658
|
+
void runWithPolishProducerDeadline(() => launchedBrowser.close(), {
|
|
659
|
+
timeoutMs: boundedCleanupDeadlineMs,
|
|
660
|
+
unrefTimer: true,
|
|
661
|
+
}).catch(() => {});
|
|
662
|
+
}
|
|
663
|
+
throw polishProducerTimeoutError();
|
|
664
|
+
}
|
|
665
|
+
return launchedBrowser;
|
|
666
|
+
});
|
|
667
|
+
browser = await runWithPolishProducerDeadline(() => startupPromise, {
|
|
668
|
+
timeoutMs: boundedStartupDeadlineMs,
|
|
669
|
+
onTimeout() {
|
|
670
|
+
startupTimedOut = true;
|
|
671
|
+
startupAbort.abort(polishProducerTimeoutError());
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
let closed = false;
|
|
676
|
+
let poisonCode = null;
|
|
677
|
+
let closePromise = null;
|
|
678
|
+
return {
|
|
679
|
+
async captureRoute({ url, viewport, signal } = {}) {
|
|
680
|
+
if (closed) throw new Error("Campaigns OS polish capture browser adapter is already closed.");
|
|
681
|
+
if (poisonCode === POLISH_PRODUCER_TIMEOUT_ERROR_CODE) throw polishProducerTimeoutError();
|
|
682
|
+
if (poisonCode === POLISH_PRODUCER_CLEANUP_ERROR_CODE) throw polishProducerCleanupError();
|
|
683
|
+
if (typeof url !== "string" || url.length > MAX_POLISH_CAPTURE_URL_LENGTH) {
|
|
684
|
+
throw new Error("Campaigns OS polish capture requires a bounded HTTP(S) capture URL.");
|
|
685
|
+
}
|
|
686
|
+
if (!Number.isInteger(viewport?.width) || viewport.width <= 0
|
|
687
|
+
|| !Number.isInteger(viewport?.height) || viewport.height <= 0) {
|
|
688
|
+
throw new Error("Campaigns OS polish capture requires a positive integer viewport.");
|
|
689
|
+
}
|
|
690
|
+
let context;
|
|
691
|
+
let session;
|
|
692
|
+
let timedOut = false;
|
|
693
|
+
let detachPromise = null;
|
|
694
|
+
let contextClosePromise = null;
|
|
695
|
+
const cleanupResources = () => {
|
|
696
|
+
if (typeof session?.detach === "function" && !detachPromise) {
|
|
697
|
+
detachPromise = Promise.resolve().then(() => session.detach());
|
|
698
|
+
// Context closure is authoritative. CDP detach commonly rejects when
|
|
699
|
+
// that same close wins the race, so observe but never await or expose it.
|
|
700
|
+
void detachPromise.catch(() => {});
|
|
701
|
+
}
|
|
702
|
+
if (typeof context?.close === "function" && !contextClosePromise) {
|
|
703
|
+
contextClosePromise = Promise.resolve().then(() => context.close());
|
|
704
|
+
}
|
|
705
|
+
if (!context) return Promise.resolve();
|
|
706
|
+
if (!contextClosePromise) return Promise.reject(polishProducerCleanupError());
|
|
707
|
+
return contextClosePromise.catch(() => { throw polishProducerCleanupError(); });
|
|
708
|
+
};
|
|
709
|
+
const assertActive = () => {
|
|
710
|
+
if (!timedOut) return;
|
|
711
|
+
poisonCode = POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
712
|
+
void cleanupResources().catch(() => {});
|
|
713
|
+
throw polishProducerTimeoutError();
|
|
714
|
+
};
|
|
715
|
+
const awaitActive = async (promise) => {
|
|
716
|
+
try {
|
|
717
|
+
const value = await promise;
|
|
718
|
+
assertActive();
|
|
719
|
+
return value;
|
|
720
|
+
} catch (error) {
|
|
721
|
+
assertActive();
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
const triggerTimeout = () => {
|
|
726
|
+
timedOut = true;
|
|
727
|
+
poisonCode = POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
728
|
+
void cleanupResources().catch(() => {});
|
|
729
|
+
};
|
|
730
|
+
if (signal?.aborted) triggerTimeout();
|
|
731
|
+
else if (typeof signal?.addEventListener === "function") {
|
|
732
|
+
signal.addEventListener("abort", triggerTimeout, { once: true });
|
|
733
|
+
}
|
|
734
|
+
let observation;
|
|
735
|
+
let operationError = null;
|
|
736
|
+
try {
|
|
737
|
+
observation = await runWithPolishProducerDeadline(async () => {
|
|
738
|
+
assertActive();
|
|
739
|
+
context = await browser.newContext({
|
|
740
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
741
|
+
serviceWorkers: "block",
|
|
742
|
+
});
|
|
743
|
+
assertActive();
|
|
744
|
+
if (authCookies.length > 0) {
|
|
745
|
+
const origin = captureOrigin(url);
|
|
746
|
+
// Fixed message: a URL containing credentials or query data is never echoed.
|
|
747
|
+
if (origin === null) throw new Error("Campaigns OS polish capture requires an HTTP(S) capture URL before applying --auth-cookie.");
|
|
748
|
+
await awaitActive(context.addCookies(authCookies.map((cookie) => ({ ...cookie, url: origin }))));
|
|
749
|
+
}
|
|
750
|
+
const page = await awaitActive(context.newPage());
|
|
751
|
+
session = await context.newCDPSession(page);
|
|
752
|
+
assertActive();
|
|
753
|
+
const collector = createNetworkCollector();
|
|
754
|
+
await awaitActive(session.send("Network.enable"));
|
|
755
|
+
await awaitActive(session.send("Network.setCacheDisabled", { cacheDisabled: true }));
|
|
756
|
+
await awaitActive(session.send("Network.setBypassServiceWorker", { bypass: true }));
|
|
757
|
+
collector.listen(session);
|
|
758
|
+
|
|
759
|
+
const navigationStartedAt = performance.now();
|
|
760
|
+
await awaitActive(page.goto(url, { waitUntil: "domcontentloaded", timeout: NAVIGATION_TIMEOUT_MS }));
|
|
761
|
+
const initialFrameTree = await awaitActive(session.send("Page.getFrameTree"));
|
|
762
|
+
const initialMainFrame = initialFrameTree?.frameTree?.frame;
|
|
763
|
+
const initialMediaElements = await awaitActive(collectMediaElements(page));
|
|
764
|
+
let networkidle;
|
|
765
|
+
try {
|
|
766
|
+
await awaitActive(page.waitForLoadState("networkidle", { timeout: NETWORKIDLE_TIMEOUT_MS }));
|
|
767
|
+
networkidle = { status: "settled", duration_ms: durationSince(navigationStartedAt) };
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (!timeoutError(error)) throw error;
|
|
770
|
+
networkidle = { status: "timeout", duration_ms: durationSince(navigationStartedAt) };
|
|
771
|
+
}
|
|
772
|
+
const finalMediaElements = await awaitActive(collectMediaElements(page));
|
|
773
|
+
await awaitActive(drainProtocolEvents());
|
|
774
|
+
const frameTree = await awaitActive(session.send("Page.getFrameTree"));
|
|
775
|
+
const mainFrame = frameTree?.frameTree?.frame;
|
|
776
|
+
const finalDocumentUrl = page.url();
|
|
777
|
+
const documentContextChanged = typeof initialMainFrame?.id !== "string"
|
|
778
|
+
|| typeof initialMainFrame?.loaderId !== "string"
|
|
779
|
+
|| initialMainFrame.id !== mainFrame?.id
|
|
780
|
+
|| initialMainFrame.loaderId !== mainFrame?.loaderId;
|
|
781
|
+
const mediaElements = mergeMediaElementSnapshots(
|
|
782
|
+
documentContextChanged ? { observed_element_count: 0, elements: [] } : initialMediaElements,
|
|
783
|
+
finalMediaElements,
|
|
784
|
+
);
|
|
785
|
+
const network = collector.finish({
|
|
786
|
+
mainFrameId: mainFrame?.id,
|
|
787
|
+
mainLoaderId: mainFrame?.loaderId,
|
|
788
|
+
finalDocumentUrl,
|
|
789
|
+
});
|
|
790
|
+
if (documentContextChanged) {
|
|
791
|
+
network.responseCollectionStatus = "failed";
|
|
792
|
+
network.responses.push(captureProblemRecord("document_context_changed"));
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
finalDocumentUrl,
|
|
796
|
+
responseCollectionStatus: network.responseCollectionStatus,
|
|
797
|
+
networkidle,
|
|
798
|
+
mediaElements,
|
|
799
|
+
responses: network.responses,
|
|
800
|
+
};
|
|
801
|
+
}, {
|
|
802
|
+
timeoutMs: boundedCellDeadlineMs,
|
|
803
|
+
onTimeout: triggerTimeout,
|
|
804
|
+
signal,
|
|
805
|
+
});
|
|
806
|
+
} catch (error) {
|
|
807
|
+
operationError = error;
|
|
808
|
+
if (error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE) {
|
|
809
|
+
poisonCode = POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
810
|
+
}
|
|
811
|
+
} finally {
|
|
812
|
+
if (typeof signal?.removeEventListener === "function") {
|
|
813
|
+
signal.removeEventListener("abort", triggerTimeout);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
let cleanupError = null;
|
|
817
|
+
try {
|
|
818
|
+
await runWithPolishProducerDeadline(cleanupResources, { timeoutMs: boundedCleanupDeadlineMs });
|
|
819
|
+
} catch (error) {
|
|
820
|
+
cleanupError = error;
|
|
821
|
+
}
|
|
822
|
+
if (cleanupError?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE) {
|
|
823
|
+
poisonCode = POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
824
|
+
throw cleanupError;
|
|
825
|
+
}
|
|
826
|
+
if (operationError?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE) {
|
|
827
|
+
poisonCode = POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
828
|
+
throw operationError;
|
|
829
|
+
}
|
|
830
|
+
if (cleanupError) {
|
|
831
|
+
poisonCode = POLISH_PRODUCER_CLEANUP_ERROR_CODE;
|
|
832
|
+
throw polishProducerCleanupError();
|
|
833
|
+
}
|
|
834
|
+
if (operationError) throw operationError;
|
|
835
|
+
return observation;
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
async close() {
|
|
839
|
+
if (!closePromise) {
|
|
840
|
+
closed = true;
|
|
841
|
+
closePromise = (async () => {
|
|
842
|
+
try {
|
|
843
|
+
await runWithPolishProducerDeadline(() => browser.close(), {
|
|
844
|
+
timeoutMs: boundedCleanupDeadlineMs,
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE) throw error;
|
|
848
|
+
throw new Error("Campaigns OS polish capture could not close its browser cleanly.");
|
|
849
|
+
}
|
|
850
|
+
})();
|
|
851
|
+
}
|
|
852
|
+
return closePromise;
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// The browser-install step spelled for the install this package runs from:
|
|
858
|
+
// the checkout script from a checkout, otherwise `qa install-browser` through
|
|
859
|
+
// the prefix that runs THIS copy (see install-mode.mjs).
|
|
860
|
+
function installBrowserHint(kind) {
|
|
861
|
+
const prefix = invocationPrefixFor(PACKAGE_ROOT);
|
|
862
|
+
const rerun = kind === "polish" ? `then rerun \`${prefix} polish capture\`.` : "then rerun the QA command.";
|
|
863
|
+
const install = prefix === "campaigns-os"
|
|
864
|
+
? "Run `npm run qa:install-browser` from the checkout (or `campaigns-os qa install-browser`),"
|
|
865
|
+
: `Run \`${prefix} qa install-browser\`,`;
|
|
866
|
+
return `${install} ${rerun}`;
|
|
867
|
+
}
|