SwiftKick Mobile leverages Asana as an Applicant Tracking System (ATS). Our recruiting manager was spending a bit of time manually copy and pasting candidate information into Asana. We saw this as a hackathon opportunity to build a tool that scrapes candidate information from recruiting platforms and imports them directly into our ATS. We decided to focus on LinkedIn to start.
Objective
The goal of this blog is not to provide a comprehensive tutorial into chrome extension development but rather give a basic overview of the extension we built and offer some learnings that would have been nice to know before starting. It may save some aspiring extension developers some time. Keep in mind that our background is primarily in mobile development so… ya.
Anatomy of a Chrome Extension

Extension flow
File Structure:
- Manifest.json (not shown)
- Defines metadata for the extension e.g. version, script definitions, permissions, icons, etc. Tells the browser what is required by the extension.
- Background.js (not shown)
- Contains listeners for browser level events. We did not need to leverage it for this extension – used primarily for logging.
- Popup.html
- The UI of the extension – presented to the user when the extension icon is clicked.
- Popup.js
- Calls
chrome.tabs.sendMessage
ondocumentReady
andFetch user data
events - Handles authentication flow with asana
- Creates and imports candidate information into asana
- Calls
- Content.js
- Runs on the page of the currently selected tab. In our case that is LinkedIn.
- Listens for
chrome.runtime.onMessage
events - Retrieves candidates LinkedIn id
- Uses LinkedIn id to call LinkedIn API for publicly available information
Print statements can be found in multiple places which caused us some confusion at the start. Printing in
Content.js
logs to the browser console, printing inPopup.js
logs to the popup console and printing inBackground.js
logs to the extension background. We created a reference toBackground.js
inPopup.js
to have consistent logging.
Scraping Candidate Info
We have a documentReady
listener on Popup.js
and a Fetch user data
button on Popup.html
which calls chrome.tabs.sendMessage()
. The Content.js
has a listener on chrome.runtime.onMessage
. When Content.js
receives this message it scrapes the candidate’s information and passes it back to Popup.js
via callback to onMessage()
. Once this is received on Popup.js
the corresponding fields on Popup.html
are populated with whatever information was available on the document.
Problem
Content.js
gives us access to the active tabs DOM. Our initial approach was to identify elements on the LinkedIn page (e.g. name, email, etc..) and populate the corresponding fields on Popup.html
. With LinkedIn, the contact information is injected once the Contact info
button is clicked. Our initial approach was to grab the href
from the Contact info
element, open the url in the background, scrape the page, and close it. With this solution, we need an arbitrary wait on page load to ensure the popup elements are injected before scraping. It would have worked but is not an ideal solution.
Solution
After going down this path a little bit we (fortunately) found an extension that performs a similar task. If it’s the web, it likely can be inspected unless the author has gone through the process of ugliyfying the code. Inspecting the extensions Content.js
, we found they were calling out to an undocumented LinkedIn API to get the candidate information. How convenient! The only scraping needed is for the user’s id. This along with the LinkedIn cookie passed as a header is all you need to get a candidate’s public information.
Importing Tasks into Asana
On Import into asana
click, the Popup.js
initiates authentication flow. Once an access token is received from asanas OAuth endpoint we have to write access to asana on behalf of the user. From there we simply need to create a task with asana’s API and import. Creating a developer app in Asana is relatively straight forward.
Problem
What is the url of an extension? We began by using asana’s built-in authentication flow. The Asana app needs a url to redirect to once authorization has been granted. We had problems getting the window/tab to close and redirect back to the tab that initiated the authorization flow. In fact, this is how the asana extension works. If authentication is required it will open the asana login page on a new tab and redirect to the asana home page on the same tab. A 3rd party asana application uses a similar approach to authentication. Either there is no native support for extension redirect in asanas authorization API or we could not find it.
Solution
Chrome has an API for getting OAuth2 tokens. The API to get the redirect url of the extension and how redirects the user back to the last active tab after authentication. launchWebAuthFlow()
opens a window for authorization and closes once granted, leaving you on the tab which initiated the flow. The only thing needed is asana’s OAuth url for getting an access token and the app’s redirect url which can be obtained via the chrome.identity
API. With the access token, we can import tasks to asana on the user’s behalf.
You can also access asana via personal access token vs. OAuth which will get you up and running much quicker. The goal of our extension is to eventually release in the Chrome Store which requires OAuth.
Uploading to the Chrome Store
Uploading an extension to the chrome store is a simple as zipping the folder and dropping it into the developer console. There is a nominal $5 fee and cursory review process that generally takes ~5 minutes.
Problem
To test this we packed and unpacked the extension on another computer. The first thing we noticed is the id of the extension changed. This was concerning because the redirect url needed when creating the asana app has the id of the asana extension.
Solution
This turned out to not be a problem because the id will be constant once uploaded and distributed through the Chrome Web store. A workaround if using a personal access token is to simply update the url every time the id changes.
Learnings
- Be mindful of where print statements are being logged
- Auditing extensions or tools that perform similar tasks may be inspectable and provide useful information
- Asana’s auth flow is great if you want the user to land on web page after authentication. Otherwise, go with chrome’s OAuth2 API’s
- The id of the extension is constant and will not change once uploaded to the Chrome Store.
Next Steps
It would be nice to adopt this to other candidate sites (e.g. Indeed) but that is dependent on them offering a public API to candidate information. If so, only our response serialization logic would need to be updated. If not we may need to go down the path of scraping information from the candidate’s page.