Rehoboth Builds

chrome.runtime.id is a different string on Edge, and it breaks your store links silently

Publishing the same zip to Edge Add-ons is genuinely close to free — the manifest works, the APIs work, nothing throws. That is exactly what makes this one expensive: the thing that breaks is a link, links do not raise exceptions, and the broken one is on the path where a user was about to leave you a rating.

9 September 2026 · found while filing the same extension in Partner Center

1. One zip, two extension IDs

A Chrome extension ID is derived from the signing key, and each store signs your upload with its own. So the same bytes, uploaded twice, come back with two different 32-character IDs, and chrome.runtime.id at runtime returns whichever one the browser installed.

That is fine until you build a URL out of it. This line is in a lot of extensions, including ours:

const url = `https://chromewebstore.google.com/detail/${chrome.runtime.id}/reviews`;

On Edge, chrome.runtime.id is the Edge ID, and that URL is a Chrome Web Store URL containing an ID the Chrome Web Store has never heard of. It resolves, it returns a page, and the page says the item is not available. No error, no console warning, nothing in your test suite — the code is doing exactly what you wrote.

2. The Edge equivalent is not the extension ID at all

The obvious fix is "detect Edge and use the Edge URL", and the obvious way to write that is to substitute the ID into the other host. That does not work either, and this is the part that cost us the most time to notice:

https://microsoftedge.microsoft.com/addons/detail/<STORE_ID>

The STORE_ID in an Edge Add-ons detail URL is a short Partner Center identifier — a dozen alphanumeric characters, nothing like a CRX ID. It is not derived from your extension ID, and there is no API that hands it to you at runtime. Which means:

  • it has to be a hardcoded constant in your source, and
  • it only exists after you create the Partner Center submission, and
  • because it is hardcoded, it ships inside the reviewed package — wrong value, full review to fix.

So it is an ordering constraint, not a coding task: create the Partner Center draft, copy the Store ID out of the dashboard URL, put it in the code, then build the zip you submit to both stores.

3. Detecting Edge, and being honest about the check

Both browsers are Chromium, so there is no capability that differs and no feature to detect. The only stable signal is the brand token in the user agent:

const IS_EDGE = /\bEdg\//.test(navigator.userAgent);

A brand-string check is the weakest kind of check, and pretending otherwise would be worse than the bug. What makes it acceptable here is the direction of failure: if the test is wrong, one link points at the wrong store. Nothing in the extension stops working, no data is lost, and the person can still find you by name. Compare that to gating a feature on the same check — same reliability, very different blast radius. Ask which one you are doing before you decide the check is good enough.

Two practical notes. User-agent reduction trims version detail, not the brand token, so Edg/ survives it. And in a popup this string is available with no permissions at all — you are reading your own window, not the page.

4. Keep the choice in a pure function

The reason this bug lived in our code for a while is that it was inline, at the point of use, in code that only runs inside a popup inside a browser. Pulling the decision out makes it a thing you can test with node in a millisecond:

export function reviewUrl(extensionId, opts) {
  if (opts && opts.edge && opts.edgeStoreId) {
    return `https://microsoftedge.microsoft.com/addons/detail/${opts.edgeStoreId}`;
  }
  return `https://chromewebstore.google.com/detail/${extensionId}/reviews`;
}

Note the && opts.edgeStoreId. If the constant is ever missing, the function falls back to the Chrome URL instead of building .../addons/detail/undefined. Degrading to "the other store's link" beats degrading to a URL with the word undefined in it, and the difference is one clause.

5. It is a family, not a bug — so grep for it

The rating link is just the instance we happened to hit. Anything in your package that names one store is the same mistake waiting in a different file:

  • the uninstall URL, if it carries a ?store=chrome style tag
  • "Rate us on the Chrome Web Store" as visible UI text — on Edge that sentence is simply false
  • support and feedback links that point at one store's listing
  • screenshots and help pages that show the other browser's UI

The check that finds all of them at once runs against the built package, not the source tree, which matters if you have a build step:

grep -rn 'chromewebstore\|chrome.google.com/webstore\|Chrome Web Store' dist/

Every hit is either deliberately Chrome-only or a bug. We ended up rewording the visible copy to be browser-neutral rather than branching it, because the branch has to be maintained in every language you translate into, and the neutral sentence has to be written once.

How we know, and what we are not claiming

We hit this on 6 September 2026 while filing the same extension in Partner Center: the review prompt built a Chrome Web Store URL from chrome.runtime.id, which is the Edge ID there. The fix in §4 and the user-agent check in §3 are the code we shipped in the following version.

What we are not claiming: we have not measured how many people hit the wrong link before the fix — that number is not knowable from a store dashboard, which is part of why this class of bug is worth a checklist rather than a metric. We also did not test every Chromium browser that reports a brand token; the argument in §3 is about the cost of being wrong, not about the check being complete.

Where this came from

It came out of Highlight Reader, a Chrome extension that reads a page aloud and highlights each sentence as it speaks. It is free on the Chrome Web Store, needs no account, and uses the voices the browser already ships. A paid add-on — word-level highlighting and higher-quality voices — is described here, and it is an add-on, not the extension.

The API and the code are the easy half. The half that costs days is the store: the slot cap on a new account, the listing text going read-only mid-review, the ID you cannot look up until the draft exists. The ten that caught us — three of them in full, no signup.