What Building a Chrome Extension Using ChatGPT Has Taught Me

I have been experimenting with ChatGPT and MidJourney for several months now in an effort to stay ahead. It’s nothing new for me as ongoing education and adapting to change are essential aspects of my career. I enjoy learning new things, hacking away, failing, and trying again.

When MidJourney’s big update, v4, was released last year, it was fascinating to see the improvement in results from MJ3 to MJ4.

MidJourney 3

MidJourney 4

When I started using ChatGPT, I assumed that progress would be similar to what I had seen with MidJourney after their update.

My daily work nowadays involves innovating in web networks. I am consistently searching for new websites in different niches and crafting various outreach and campaign angles.

Recently, I had an idea to create a Chrome Extension that would show a visible traffic ranking in the extension as I navigated around the web. This way, I could quickly deduce how popular a site might be. I used to get this information from a Compete chrome extension, but they have since moved on.

Majestic has a handy MajesticMillion data set that I was able to use to power this mini-application. It contains 1,000,000 websites ordered by traffic levels.

After running some initial prompts on ChatGPT, I got stuck after encountering 5-8 different de-bugs. I would commonly start stepping through and printing debug messages to find where the program wasn’t working, but that wasn’t always helpful. There would be some error that I lacked the knowledge to get past. Clearly, I don’t know what I’m doing beyond asking the right questions and sifting for when ChatGPT is making nonsense up and or just doesn’t know b/c the data isn’t recent enough.

I tried using ChatGPT 3 and 3.5, but the results weren’t as helpful. However, when I tried ChatGPT 4, it was trying to write new code that I hadn’t seen before, and it felt like it was getting closer to solving the issue right away. The big moment that helped was when I showed CGPT the example data in the JSON file I needed to reference. CGPT was able to make some small changes to the background.js file, and it started working.

I think that if I hacked with these Chrome extensions enough, I could start building up some actual knowledge of how they function without simply memorizing code. I don’t see myself as a Chrome Extension expert, nor someone who could even speak to what is happening inside the code that was written. But from a proof of concept standpoint and being able to push activities forward, GPT represents something like a spreadsheet to me in value.

Where my exploration started, this taught me roughly what files were necessary to launch an extension.

initial manifest.json file

{
  "manifest_version": 2,
  "name": "Your Extension Name",
  "version": "1.0",
  "description": "A brief description of your extension",
  "permissions": [
    "activeTab",
    "storage",
    "https://*/"
  ],
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content.js"
      ]
    }
  ]
}

initial content.js file

var xhr = new XMLHttpRequest();
xhr.open('GET', 'traffic.csv', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    var csv = xhr.responseText;
    var data = Papa.parse(csv, {header: true}).data;
    var trafficStats = {};
    data.forEach(function(row) {
      trafficStats[row.website] = row.traffic;
    });
  }
};
xhr.send();

GPT 4 Prompts and Results

At this point, I had stepped through logically how to do this, I realized referencing a JSON file of data would perform better than scanning a 1 million row CSV file 🙂

Updated manifest.json file (it still initially forgot to call the JSON file, which took a later update)

{
  "manifest_version": 2,
  "name": "Traffic Score",
  "version": "1.0",
  "description": "Display traffic score for the current domain in the Chrome badge",
  "icons": {
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "permissions": [
    "tabs",
    "storage",
    "activeTab",
    "http://*/*",
    "https://*/*"
  ],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "content_scripts": [
    {
      "matches": ["http://*/*", "https://*/*"],
      "js": ["content.js"]
    }
  ],
  "browser_action": {
    "default_icon": "icon48.png"
  },
  "web_accessible_resources": [
    "trafficStats.json"
  ]
}

new background.js file

let trafficStats = {};

function fetchData() {
  fetch(chrome.runtime.getURL('trafficStats.json'))
    .then(response => {
      if (!response.ok) {
        console.error('Error fetching trafficStats.json:', response.status, response.statusText);
      }
      return response.json();
    })
    .then(data => {
      trafficStats = data.reduce((acc, item) => {
        acc[item.COLUMN2] = item.COLUMN1;
        return acc;
      }, {});
      console.log('trafficStats:', trafficStats);
    })
    .catch(error => console.error('Error processing trafficStats.json:', error));
}

chrome.runtime.onInstalled.addListener(fetchData);
chrome.runtime.onStartup.addListener(fetchData);

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    chrome.tabs.sendMessage(tabId, { command: 'fetchTrafficScore' });
  }
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.command === 'updateBadge') {
    const { score } = request;
    chrome.browserAction.setBadgeText({ text: score.toString(), tabId: sender.tab.id });
    chrome.browserAction.setBadgeBackgroundColor({ color: '#4CAF50' });
    console.log(`Badge updated for domain: ${sender.url}, Score: ${score}`);
  }
});

chrome.runtime.onConnect.addListener(port => {
  console.assert(port.name === 'trafficScoreChannel');

  port.onMessage.addListener(request => {
    if (request.command === 'fetchTrafficScore') {
      const currentDomain = request.currentDomain;
      const score = trafficStats[currentDomain] || 'N/A';
      port.postMessage({ command: 'updateBadge', score });
    }
  });
});

new content.js file

function getCleanDomain(domain) {
  return domain.replace(/^www\./, '');
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.command === 'fetchTrafficScore') {
    const currentDomain = getCleanDomain(new URL(location.href).hostname);
    console.log(`Current domain: ${currentDomain}`);

    const port = chrome.runtime.connect({ name: 'trafficScoreChannel' });
    port.postMessage({ command: 'fetchTrafficScore', currentDomain });

    port.onMessage.addListener(response => {
      if (response.command === 'updateBadge') {
        const score = response.score;
        console.log(`Score for current domain: ${score}`);
        if (score !== 'N/A') {
          chrome.runtime.sendMessage({ command: 'updateBadge', score }, response => {
            if (chrome.runtime.lastError) {
              console.error('Error updating badge:', chrome.runtime.lastError);
            } else {
              console.log('Badge updated successfully.');
            }
          });
        } else {
          console.warn('No score found for the current domain');
        }
      }
    });
  }
});

There are a lot of ways to improve this from here. It was about 20 minutes of effort when I tried it with GPT4. I can’t imagine having a 70mb json file is ideal, pulling this data from an API sounds like a much better way to approach this, perhaps that is another days work.