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.
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
- 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.
- The UI of the extension – presented to the user when the extension icon is clicked.
Fetch user dataevents
- Handles authentication flow with asana
- Creates and imports candidate information into asana
- Runs on the page of the currently selected tab. In our case that is LinkedIn.
- Listens for
- 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.jslogs to the browser console, printing in
Popup.jslogs to the popup console and printing in
Background.jslogs to the extension background. We created a reference to
Popup.jsto 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
Content.js has a listener on
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.
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.
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
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.
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.
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.
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.
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.
- 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.
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.