Today, after having been in this business for more than five years, I realized that a small percentage of people who installed our GMass Chrome extension never got it to work. Why? Because our buttons never showed up for them. Why? Because the extension’s “content script” never ran. In this article, I’ll dig into the mistake I was making and how I fixed it. If you’re developing a Chrome extension for Gmail, this is a critical concept to understand.
The three timing options for content scripts
When you designate your content script in your manifest.json file, there are three timing options for when your content script should run. They are:
- document_idle
- document_start
- document_end
Google explains them in detail. They recommend that most extensions use document_idle, which fires in between the time the DOM is loaded and right after the window is loaded. However, if you’re developing an extension specifically for Gmail where you’re manipulating the Gmail DOM (Document Object Model), this will get you into trouble. In fact, all three of these can cause your script never to run. A complicating factor comes into play if you’re using the awesome Inbox SDK library with your extension.
Inbox SDK recommends that you remotely load your content script via its “loader” function. This is a beautiful concept because it allows you to make major changes to your extension without having to update your package with the Chrome Web Store and then wait for approval, and then wait again for all of your users’ browsers to get the update. The downside of this approach, however, is that your script could run later than you expect. The big flaw I discovered today was that in my content script, I had all my code wrapped in a window.onload event, like so:
window.onload = function(){ GMassReady(); } function GMassReady(){... }
In my manifest.json, I had my content script set to run at document_end:
"content_scripts": [ { "js": [ "inboxsdk.js", "gmass.js"], "matches": [ "http://mail.google.com/*", "https://mail.google.com/*" ], "run_at": "document_end" } ],
This worked for most users, but didn’t work for some. If your browser loaded Gmail particularly fast but loaded my remote content script particularly slowly, then the main code in the content script would never run. Why? Because window.onload would never fire, as it had already fired long before the remotely loaded content script was, well…remotely loaded. So window.onload fired before the script even existed in the browser’s scope.
Now, one solution to this problem is simply to package the script as part of the extension and stop loading it remotely. Then, using the window.onload wrapper would work, because it would guarantee that the script would be available by the time window.onload ran. However, I didn’t want to do that for the reason stated above.
How Gmail loads differently
The confusion for me in reading Google’s documentation on the run_at setting is that the rules for Gmail are different. The documentation says that if you’re using document_idle, you don’t need to wait for window.onload in your content script. This is wrong. Based on this, you might think you can use document_end and then call your content script, but this won’t work well either. That’s because the way the Gmail interface loads, the document_end event doesn’t actually fire when the DOM is ready, as the documentation states. Let’s prove this.
Here’s what happens when the script is run locally using document_end, so waiting until after the DOM is supposedly ready.
"run_at": "document_end"
Chrome extension world vs. real life
The “document_end” option corresponds to the real-life JavaScript “DOMContentLoaded” event. Let’s look at the exact definitions of each.
document_end: “after the DOM is complete, but before subresources like images and frames have loaded.”
DOMContentLoaded: “The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.”
See the similarities? So theoretically, if you are using a local content script, and you used document_start in manifest.json and wrapped your content script code inside a DOMContentLoaded event, that is the same thing as using document_end in your manifest.json and not wrapping your script inside a DOMContentLoaded event. If you’re using a remote content script, these rules go out the window because you simply don’t know how long it will take to load your script from its server. It’s possible that your script loads after the DOMContentLoaded event has already fired, and so wrapping your code in that event would cause it never to run.
So what’s the best approach?
The optimal setting, then, regardless of whether your content script is local or remote, is to set the script to document_start in manifest.json and then in your actual content script, wrap your code in an if/then condition based on whether or not window.onload has fired yet. By using document_start, it gets your script into the browser’s context as quickly as possible, and by using the if/then window.onload logic, it allows your script to run after it’s loaded if Gmail is ready or wait until the Gmail interface is ready. You could use document_idle or document_end, but that will just delay the inevitable…your script running. As long as you’re wrapped in a window.onload if/then checking system, you’re good.
Here’s the code:
if (document.readyState === "complete") { GMassReady(); } else { window['onload'] = function () { GMassReady(); } } function GMassReady(){...
Note: You might notice that my extension still uses document_end. Why? Because I haven’t updated my Chrome extension package yet in the Chrome Web Store. A few years ago, it was a simple process. You just uploaded a new package, and the Chrome Web Store updated — then worldwide, everyone’s browsers updated over the next couple of days. Now there’s a strict human review process before changes go live. Since I was able to fix my issue by altering my content script to include the new window.onload logic, that suffices for now.
What do other Gmail Chrome extensions do?
It turns out none of the other major Gmail extensions I tested use the default document_idle setting. As with many things in life, it seems I was the last to know about this. Let’s look at the manifest.json files of a couple of other extensions.
Mailtrack
Here’s the relevant portion of manifest.json:
content_scripts": [ { "matches": [ "https://mail.google.com/*" ], "js": [ "scripts/lib/intercom-snippet.js", "scripts/lib/snowplowSnippet.js" ], "run_at": "document_start" }, { "matches": [ "https://mail.google.com/*" ], "js": [ "scripts/gmail.js" ], "run_at": "document_start" }, { "matches": [ "https://mail.google.com/*" ], "js": [ "scripts/bundles/gmail.start.bundle.js" ], "run_at": "document_start" }, { "matches": [ "https://mail.google.com/*" ], "css": [ "styles/style.css" ], "run_at": "document_end" }, { "matches": [ "https://mail.google.com/*" ], "js": [ "scripts/bundles/gmail.end.bundle.js" ], "run_at": "document_end" }, { "matches": [ "*://mailtrack.io/*/dashboard/welcome*", "*://mailtrack.io/*/dashboard/reauthorized*", "*://mailtrack.io/*/dashboard/install-success*", "*://mailtrack.io/*/dashboard/payment/teams/success*" ], "js": [ "scripts/bundles/setup.bundle.js" ], "run_at": "document_start" }, { "matches": [ "*://mailtrack.io/*" ], "js": [ "scripts/bundles/dashboard.bundle.js" ], "run_at": "document_end" } ],
I haven’t dug into each content script here, but we can see that none of them use Google’s recommendation of document_idle.
Mixmax
Here’s a pertinent snippet of manifest.json:
"content_scripts": [ { "matches": [ "*://*.mixmax.com/*" ], "exclude_matches": [ "*://*.mixmax.com/public/analyticsbridge.html" ], "js": [ "src/content/globals.js", "src/assets/lib/raven-3.3.0.js", "src/assets/lib/Environment.js", "src/assets/lib/raven-config.js", "src/assets/lib/error.js", "src/content/ExtensionMessageBus.js" ], "all_frames": true, "run_at": "document_start" }, { "matches": [ "*://mail.google.com/*" ], "js": [ "src/content/unblock.js" ], "run_at": "document_start" }, { "matches": [ "*://mail.google.com/*", "*://*.force.com/*", "*://*.salesforce.com/*" ], "js": [ "src/content/globals.js" ], "run_at": "document_start" }, { "matches": [ "*://mail.google.com/*" ], "js": [ "src/content/pageInterop.js" ], "run_at": "document_end" }, { "matches": [ "*://mail.google.com/*", "*://*.force.com/*", "*://*.salesforce.com/*" ], "js": [ "src/assets/lib/raven-3.3.0.js", "src/assets/lib/Environment.js", "src/assets/lib/raven-config.js", "src/assets/lib/error.js", "src/content/ExtensionMessageBus.js", "src/content/app.js" ], "run_at": "document_end" }, { "matches": [ "*://www.linkedin.com/sales/widget/*" ], "js": [ "src/content/globals.js" ], "all_frames": true, "run_at": "document_start" }, { "matches": [ "*://www.linkedin.com/sales/widget/*" ], "js": [ "src/assets/lib/raven-3.3.0.js", "src/assets/lib/Environment.js", "src/assets/lib/raven-config.js", "src/assets/lib/error.js", "src/content/app.js" ], "all_frames": true, "run_at": "document_end" }, { "matches": [ "<all_urls>" ], "exclude_matches": [ "*://mail.google.com/*" ], "all_frames": true, "js": [ "src/content/mailTo.js", "src/content/callTo.js" ], "run_at": "document_idle" } ],
Again I haven’t dug into each one, but none of them use document_idle except the last one, which is the one script that does not run inside Gmail.
In Conclusion…
Developing an extension for Gmail is different than for other sites. We love Inbox SDK, and we love the ability to load content scripts remotely, but it’s important to get the timing right. Set your manifest.json to use document_start and wrap your content script code in an if/then that checks for the window.load event.
Send incredible emails & automations and avoid the spam folder — all in one powerful but easy-to-learn tool
TRY GMASS FOR FREE
Download Chrome extension - 30 second install!
No credit card required
I’m struggling with this problem for a couple of days. I had to put a setTimeout() to “patch” the problem temporarily.
Great article, very helpful.