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=chromestyle 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.
More from the same build
Every one of these is something we hit while shipping the two extensions on this site, written up from our own code and measurements.
- suggested_key is a suggestion: commands.getAll() can hand you an empty shortcut
- Your extension’s keyboard shortcut dies when focus is inside an iframe
- A timeout watchdog will fire while the user has it paused
- A saved DOM Range does not survive a repaint, and nothing tells you
- chrome.i18n formats nothing, and getUILanguage() is not the locale your strings came from
- Reading a page aloud in Chrome and highlighting each sentence