Skip to content

Chrome Web Store (semi-correctly!) rejects extension due to "Including remotely-hosted code in a Manifest V3 item." due to dynamic endpoint construction AND mentions of remotely hosted assets. #1916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
amarchen opened this issue Apr 23, 2025 · 7 comments

Comments

@amarchen
Copy link

Looking at similar issues such as #1882 it seems like Google is tightening it's extension specific rules.
Here's the rejection I've got:

Violation:
    Including remotely hosted code in a Manifest V3 item.
    Violating Content:
        Code snippet: assets/chunk-9de81f75.js: "get apiHost() { var e = this.instance.config.api_host.trim().replace(/\/$/, ""); return e === "https://app.posthog.com/" ? "https://us.i.posthog.com/" : e } get uiHost() { var e, t = (e = this.instance.config.ui_host) === null || e === void 0 ? void 0 : e.replace(/\/$/, ""); return t || (t = this.apiHost.replace(".".concat(Ao), ".posthog.com")), t === "https://app.posthog.com/" ? "https://us.posthog.com/" : t } get region() { return this._regionCache[this.apiHost] || (/https:\/\/(app|us|us-assets)(\.i)?\.posthog\.com/i.test(this.apiHost) ? this._regionCache[this.apiHost] = gn.US : /https:\/\/(eu|eu-assets)(\.i)?\.posthog\.com/i.test(this.apiHost) ? this._regionCache[this.apiHost] = gn.EU : this._regionCache[this.apiHost] = gn.CUSTOM), this._regionCache[this.apiHost] } endpointFor(e) { var t = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : ""; if (t && (t = t[0] === "/" ? t : "/".concat(t)), e === "ui") return this.uiHost + t; if (this.region === gn.CUSTOM) return this.apiHost + t; var n = Ao + t; switch (e) { case "assets": return "https://".concat(this.region, "-assets.").concat(n); case "api": return "https://".concat(this.region, ".").concat(n) } }"

Certainly, I can't be sure, but it looks like it's the fact that URL is constructed dynamically is what triggers chrome webstore heuristics.
Could it be possible to somehow specify it during build time for .no-external imports?

At the moment I am importing posthog the following way:

import "posthog-js/dist/surveys"
import "posthog-js/dist/exception-autocapture"
import "posthog-js/dist/tracing-headers"
import "posthog-js/dist/web-vitals"
import posthog from 'posthog-js/dist/module.no-external'

(not using import posthog from 'posthog-js/dist/module.full.no-external due to #1464

@amarchen
Copy link
Author

After a second look at the google's warning, maybe they are not so wrong in fact. I do not use Posthog feature flag and don't really know how they work, but judging from just the warning snippets is it about serving different assets under different feature flag? I guess if I used feature flags, then it would count as remotely hosted code indeed.

Then the real issue is in that even "module.no-external" version still has runtime dependency on possible externally hosted assets. It would be good to able to get a build without it. To be extra-safe for just extension I would even skip runtime apiHost resolution and set it during build-time if it's possible in JavaScript builds.

Here's BTW Google warning in a more readable layout.

get apiHost() {
    var e = this.instance.config.api_host
        .trim()
        .replace(/\/$/, "");

    return e === "https://app.posthog.com/"
        ? "https://us.i.posthog.com/"
        : e;
}

get uiHost() {
    var e, 
        t = (e = this.instance.config.ui_host) === null || e === void 0
            ? void 0
            : e.replace(/\/$/, "");

    return t || (t = this.apiHost.replace(".".concat(Ao), ".posthog.com")),
        t === "https://app.posthog.com/"
            ? "https://us.posthog.com/"
            : t;
}

get region() {
    return this._regionCache[this.apiHost] ||
        (
            /https:\/\/(app|us|us-assets)(\.i)?\.posthog\.com/i.test(this.apiHost)
                ? this._regionCache[this.apiHost] = gn.US
                : /https:\/\/(eu|eu-assets)(\.i)?\.posthog\.com/i.test(this.apiHost)
                    ? this._regionCache[this.apiHost] = gn.EU
                    : this._regionCache[this.apiHost] = gn.CUSTOM
        ),
        this._regionCache[this.apiHost];
}

endpointFor(e) {
    var t = arguments.length > 1 && arguments[1] !== void 0
        ? arguments[1]
        : "";

    if (t && (t = t[0] === "/" ? t : "/".concat(t)), e === "ui") {
        return this.uiHost + t;
    }

    if (this.region === gn.CUSTOM) {
        return this.apiHost + t;
    }

    var n = Ao + t;

    switch (e) {
        case "assets":
            return "https://".concat(this.region, "-assets.").concat(n);
        case "api":
            return "https://".concat(this.region, ".").concat(n);
    }
}

@amarchen amarchen changed the title Chrome Web Store rejects extension due to "Including remotely-hosted code in a Manifest V3 item." which is most probably due to dynamic endpoint construction Chrome Web Store (semi-correctly!) rejects extension due to "Including remotely-hosted code in a Manifest V3 item." due to dynamic endpoint construction AND mentions of remotely hosted assets. Apr 29, 2025
@pauldambra
Copy link
Member

Hey,

It's not clear to me why you're not using the full package...

but the no-external bundle only exists because of this chrome store validation 🤷

could you create a minimal reproducible example that shows the bundled output includes the remote loading code (since how I would choose to bundle it might be importantly different to how you would)

@pauldambra
Copy link
Member

others have seen success with these imports

#1882 (comment)

@amarchen
Copy link
Author

amarchen commented May 2, 2025

My last attempt was with just the following imports:

// import posthog from 'posthog-js/dist/module.full.no-external'
// import posthog from 'posthog-js/dist/module.full'

// Recorder is disabled, because of
// import "posthog-js/dist/recorder"
// import "posthog-js/dist/surveys"
// import "posthog-js/dist/exception-autocapture"
// import "posthog-js/dist/tracing-headers"
// import "posthog-js/dist/web-vitals"
import posthog from 'posthog-js/dist/module.no-external'

Notice how I have commented everything except the minimal one (to my understanding it's minimal possible) and still got the same rejection. I have even tried appealing and got the same rejection with the same reason and code excerpt was exactly about dynamic calculation of apiHost and uiHost.

it's google, you are always in some AB-testing bucket I guess and hard to know what's the dominant rules are going to be like. I can only guess that this piece of code indeed looks like posthog is going to load and external and possibly executable asset. I am thinking of trying to hard-replace it with hard coded values manually in a submitted build and see if it helps.

@pauldambra
Copy link
Member

thanks

could you create a minimal reproducible example that shows the bundled output includes the remote loading code (since how I would choose to bundle it might be importantly different to how you would)

@amarchen
Copy link
Author

amarchen commented May 2, 2025

Unfortunately that would be hard for me, I'll see if I manage :/

However, I have also traced to where this code is coming from (well, in whole posthog JS code there seems to be the only place where such functions are defined) - https://github.com/PostHog/posthog-js/blob/main/src/utils/request-router.ts

I suppose that the router is included into all the build variants and most (all?) bundlers-minifiers won't inline it, so this code will stay in the final build.

@amarchen
Copy link
Author

amarchen commented May 5, 2025

And it worked! After several rejections once I manually edited the build to "inline" these apiHost() and uiHost() functions app store has approved it and I can already see events flowing in from the approved version!

You can see how it works in this extension LinkedIn Post Generator which is yet another wrapper over OpenAI API, but with a cool twist that instead of generating usual AI crap, it helps you state your point and you can improve-improve-improve drafts by clicking a button (such as "Make it more negative").

Anyways, you can see in the generated build (e.g. via CRX extension - not mine, it's just the easiest way to see source code if extension right in store) the final approved code for these apiHost and uiHost:

   get apiHost() {
        return "https://us.i.posthog.com"
    }
    get uiHost() {
        return "https://us.posthog.com"
    }

These were literally only two things I changed manually in the final build. I think these are exactly the values that would have been evaluated for my config anyways, just inlined.

Unfortunately my JS Build skills are not good enough to figure if vite builder that I use (or any other one) can be configured to inline functions that much. My current not overeducated understanding is that most of (all?) builders will keep the code more or less close to original as per #1916 (comment) , because it essentially is supposed to be dynamic code changeable at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants