Building Grafiki
Open blogpost about the whole journey of building Grafiki, an app that lets you manage your ad campaigns on Quora, Snap, Pinterest, and more.
If you'd like to beta test Grafiki for iOS, sign up here: https://docs.google.com/forms/d/1YD1eI-gh25_pnZX5DkvBGKS9yPPPYsy9EdLpJCKwlP0/edit
Open blog about building Grafiki. Writing helps me clear my head and solve obstacles I come across. These blogposts might not make sense for you to read, but for me they help me to clear the fog in my head.
25 November 2023
13:33 - Writing this blogpost so I have a link to use in my Quora ads. I used Adobe Firefly to create an image of a mandril with a sunset in the background. That's the image you can find at the top of this page.
14:12 - Just created test ads for Pinterest, Quora, Snap, and LinkedIn. It's crazy how fast you can do that these days. Just add a headline, body, image and a link. Select some people to target, add your credit card and done. Quora and LinkedIn review seem to take at least 24 hours, especially in the weekend, but Snap and Pinterest went live fast. These are not the best ads and are actually not meant to bring in users. I just need some test data in my own ad accounts, so when I actually use the APIs I get some proper data back instead of zeroes everywhere.
14:17 - Updated blogpost. I added the Quora API recently already to the backend. Now gonna start with Pinterest.
I basically have two Graphql endpoints: a getSubstructures endpoint and an analytics endpoint. The analytics endpoint is for later, because I'll need to study the different APIs more.
The getSubstructures endpoint allows me to - as the name suggests - get the substructures for an entity. For example you have a LinkedIn campaign group with ID 1234, I can use the endpoint in the app to fetch all the substructures for that campaign group, which are campaigns. Other ad networks don't have campaign groups, but instead use campaigns at that level.
I want as little logic in the app as possible, everything should be very dynamic. It's up to the backend to do the heavy lifting and give the data back as uniform as possible. I don't want to have logic like this in the app:
if (adNetwork == 'Linkedin') {
header = 'Campaign'
} else if (adNetwork == 'Pinterest') {
header = 'Ad Group'
} else if (adNetwork == 'Snap') {
header = 'Ad Squad'
}
// ... etc
15:51 - Back after a little Duolingo intermezzo. If you want to connect on Duolingo, add me: GMJelle. Now back to Pinterest ads API.
Just noticed that LinkedIn already spent 6 euros for 54 website clicks. Pretty good result actually! If you're reading this through LinkedIn (or any other ad), you can sign up here to betatest the app soon: https://docs.google.com/forms/d/1YD1eI-gh25_pnZX5DkvBGKS9yPPPYsy9EdLpJCKwlP0/edit
Snap ad was not yet approved, the Quora ad was delivering but no clicks yet.
With Quora ads you basically get both analytics and entity details in one API call. Pinterest is gonna make me do two calls, one for the analytics, and another one to get the entity details like the name (of the ad group for example). Don't like it but it is what it is.
Pinterest API does not allow me to get analytics for all Ad Accounts in 1 call, unlike with campaigns and ad groups. So won't bother showing Analytics data for Ad Accounts, as it will result in an n+1 problem.
16:29 - coffee time!
16:33 - back at the desk. I'm having a bit of trouble staying focused because I try to keep it as dynamic as possible. The names across all APIs are different, with the only similarity being that the lowest level is called Ad across all networks. The level above can be called Ad Squad, Ad Set, Ad Group, etc. So instead of calling the lowest level "Ad", and the second level "Ad Squad/Set/Group", I decided to just count my way up:
Level 0: Ads
Level 1: Ad set/group/squad or Campaign (looking at you LinkedIn!)
Level 2: Campaign or Campaign Group
Level 3: (Ad) Account
Level 4: Organization, Business account, or User
It just makes it a bit difficult and abstract to think: If I want the substructure of Level 3 of Pinterest, what am I fetching? Answer: Campaigns
I have a table in Notion mapping all entities, but still, I lose my train of thoughts sometimes.
17:44 - Ordered food, it's here in 6 minutes. Then gonna eat, play chess and afterwards continue. It's going slow, but it's going.
18:42 - Let's get back to the matter at hand: Pinterest Ads API!
20:52 - I don't really like the code I wrote but it works. It's pretty abstract and I don't like the fact that it's possible for the Pinterest Ads API to return no analytics data at all for e.g. a campaign if it has no data. Instead of giving zeroes back, it just doesn't return anything. But I still want to have zeroes in the data I send towards the client, so I need to fill those up.
21:22 - Okay the above example was actually easily solved. But still!!1! Not easy to do all this dynamically and clean. This is not an excuse for my bad coding skills (it is).
26 November 2023
00:32 - Pinterest is now completely done. There was a little issue with the filtering, but that is solved now. Hooray for TDD! Now I'm gonna prepare the LinkedIn API for tomorrow. I'm reading through the docs, see how the structures are built and then add some API requests in Bruno (a Postman alternative)
13:48 - Had breakfast, did my mobility exercises an took a nice shower. Then did some Duolingo and played chess, and I'm ready to continue this little wrestle with the LinkedIn API. But first... coffee!
18:39 - That will be it for this weekend. The LinkedIn API is pretty odd, and I keep getting 400 status errors, even when executing Postman requests they provide or by using cURL requests I copy-paste from the docs. Hopefully I can make some progress this week.
23:59 - I was really tired around 22:30, but got a sudden burst of energy. I put on some candles and am sitting behind my desk, going to get the LinkedIn API to work. Not gonna spend hours on it obviously, but it would be nice knowing what goes wrong with the analytics calls.
27 November 2023
00:31 - Holy shit is the LinkedIn API one big mess. I see API examples online for calls to:
https://api.linkedin.com/v2/...
and
https://api.linkedin.com/rest/...
And then you have the Linkedin-version header you have to add, but also a X-Restli-Protocol-Version.
And if you want to use the v2 endpoint, this example from 1 year ago says that instead of using adAccounts you have to use adAccountsV2. But there's nothing on the official LinkedIn marketing documentation website to be found about the V2 suffix.
And then you have the Postman collections you can use (which use the /rest/ path, so I assume that one is the correct one). However, those requests give back errors about missing parameters.
28 November 2023
19:00 - Finished work and dinner. Going to try to untangle the LinkedIn Analytics API one more time. Let's hope I get it to work today!
22:11 - Literally did nothing. The Sinquefield cup (chess) started so I've been watching that on YouTube.
2 December 2023
00:30 - Gonna map the Snap API in Bruno and then go to bed.
3 December 2023
19:51 - Went out with family to celebrate my birthday. Came home, took some time to wind down and eat something. I'm now in my lounge chair with a crackling candle and a cup of hot tea next to me, while some lo fi beats are playing on the TV. Let's see if I can finish writing tests for the LinkedIn API today and maybe implement the substructures
endpoint as well!?
9 December 2023
18:32 - It's been a while. I kinda forgot about updating this blogpost to be honest.
So where are we? How have you been? All good?
I've been working since noon today. A couple of days ago I found an amazingly clean way to transform the API requests from the ad networks into a uniform API request for the Grafiki API: Pandas!
I load it all into one or two dataframes, merge them, clean everything up and then just .iterrows()
factory the rows into dataclasses and tada! No more for loops, checking if keys are in the response, etc. I transformed around 150 lines of pretty complex manual code into 24 lines of Pandas magic for the Snap and Quora API. To see what transformations I have to do I have a Jupyter Notebook per platform, the API mock ad network requests and the result the results the Grafiki internal API should return. TDD!!!
So I'm going to continue transferring the other platforms to this Pandas way. And then the API to get the substructures is done. It didn't go as fast as I want, I got some trouble focussing and staying motivated. Especially after work I don't have the energy left to work on this. That's why the weekends are for hustling.
I'm gonna order food now and I'll give you an update in a couple of hours. Talk to you later!
20:36 - Took a little break and updated my blog to Ghost V5. Woohoo inline code!
. And emoji search when typing a colon π. Just what I needed to keep you all up to date. Be prepared for a ton more emojis from now on! But for now... back to business! πΌπ¨βπ»π’
22:40 - Listening Lofi & Chess.com's chill beats. I feel I'm super focused, it's going great. Snap and Quora are using the Pandas way of working and it's so much fun and so easy to work with. Right now I'm gonna map the Pinterest API!
10 December 2023
02:23 - The LinkedIn API is a pain in the... Let me show you what I mean.
So first of all, they have this new way of filtering things throught the API. Imagine you have an API, and you want to fetch the ads from the campaigns with ID urn:li:23
and urn:li:456
, and you want no test_data
and you want to order everything by ID. Normally it would look something like this:
https://myapi.com?campaign_ids=urn:li:123,urn:li:456&test_data=false&order_by=id
Makes sense, right?
Now the new LinkedIn API wants you to send your query like this:
https://api.linkedin.com/rest/adAccounts/XYZ/adCampaigns?q=search&search=(test:false,campaigns:(values:List(urn:li:123,urn:li:456)))&sort=(field:ID,order:DESCENDING)
Which looks clean, but it works like shit. Because it gets urlencoded into this by for example Postman and Python's requests:
https://api.linkedin.com/rest/adAccounts/XYZ/adCampaigns?q=search&search=%28test%3Afalse%2Ccampaigns%3A%28values%3AList%28li%3A123%2Curn%3Ali%3A456%29%29%29%0&sort=%0A%28field%3AID%2Corder%3ADESCENDING%29
And the API of LinkedIn can't handle the different kinds of colons: the first colon after 'test' acts like an equal sign (=), but then you have colons in the IDs (urn:li:123) and then it thinks the colons also act like equal signs.
Anyway, it's getting late, let's this dev session with a rant, and go to bed. Goodnight! Dobranoc!
14:42 - Alright! Are you ready to delve in the mysterious LinkedIn API with its many traps and odd characteristics? Want to join me and discover all ins and outs of it, every little bit, so we can finally leave this part of Grafiki behind us? If you said yes, then fasten your seatbelt, and get ready. The road is gonna be bumpy but rewarding. Vamos!
21:56 - Just a heads up for all API developers. If your examples on your documentation pages don't work, you messed up. Yes, I'm (still) looking at you LinkedIn!