Building Grafiki

●9 MIN READ

Building Grafiki

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.

LinkedIn ad

Snap ad

Quora ad

Pinterest ad

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!

More polaroids I want to share

CLICK OR DRAG THE TOP ONE ASIDE

You've reached the end of the shoebox. More polaroids and other posts live on my Instagram.

@GMJELLE →
Polaroid of a calm lake at dusk reflecting pink clouds and dark hills, at a wild campsite near Adana, Turkey
Polaroid of Josip Broz Tito's marble tomb seen through plant leaves in Belgrade, Serbia
Polaroid of a motorcycle parked at a Soviet-era bus stop decorated with a flower mosaic in Moldova
Polaroid of a large stone statue of a bearded man in a long coat on a hillside near Buzludzha, Bulgaria
Polaroid of the saucer-shaped Buzludzha Monument on a distant hilltop above the trees in Bulgaria
Polaroid of a man standing in front of the Buzludzha Monument and its tower in Bulgaria
Polaroid of a valley of pale, cone-shaped rock formations in Cappadocia, Turkey
Polaroid of hot air balloons silhouetted against the dawn sky in Cappadocia, Turkey
Polaroid of the brutalist Cosmos Hotel tower in Chisinau, Moldova
Polaroid of the Tomb of Cyrus the Great on its stepped stone base at Pasargadae, Iran
Polaroid of a close view of the Tomb of Cyrus the Great at Pasargadae, Iran
Polaroid of a converted red fire truck camper and a motorcycle at a campsite on the west coast of Turkey
Polaroid of a mosque minaret and domes silhouetted against a cloudy sky in Edirne, Turkey
Polaroid of the Mimar Sinan statue in front of the Selimiye Mosque in Edirne, Turkey
Polaroid of the courtyard of a historic house with a fountain and colored-glass windows in Isfahan, Iran
Polaroid of a man standing next to an Isfahan city emblem sign on a sidewalk in Isfahan, Iran
Polaroid of the tiled dome and entrance portal of the Sheikh Lotfollah Mosque in Isfahan, Iran
Polaroid of the tiled entrance portal and twin minarets of the Shah Mosque in Isfahan, Iran
Polaroid of a columned palace silhouetted at sunset behind a fountain pool on Naqsh-e Jahan Square, Isfahan, Iran
Polaroid of a dark interior lit through colorful stained-glass windows in a historic house in Isfahan, Iran
Polaroid of rocky ridges and rock formations under a cloudy sky near Goreme, Cappadocia, Turkey
Polaroid of a loaded motorcycle parked by a desert road with mountains behind it in Iran
Polaroid of a loaded motorcycle with a helmet on the mirror, parked on a coastal road near Izmir, Turkey
Polaroid of a Soviet-era mosaic of workers and a motorcyclist on a building wall in Lutsk, Ukraine
Polaroid of a Soviet-era mosaic of a crowned woman holding scales, framed by tree leaves in Lviv, Ukraine
Polaroid of the Lviv Opera House facade with a fountain in front in Lviv, Ukraine
Polaroid of a Soviet WWII memorial column with a stork's nest on top in northern Moldova
Polaroid of a large Soviet-era mural of figures on a wall beside a tree in Moldova
Polaroid of royal tombs carved into the cliff face at Naqsh-e Rustam near Persepolis, Iran
Polaroid of a stone relief of a Persian soldier holding a spear at Persepolis, Iran
Polaroid of the Gate of All Nations with its winged bull statues and columns at Persepolis, Iran
Polaroid of four men sharing dinner at a table in a traditional hostel in Shiraz, Iran
Polaroid of the tiled courtyard and reflecting pool of the Nasir al-Mulk (Pink) Mosque in Shiraz, Iran
Polaroid of two men smiling at a restaurant table in Tabriz, Iran
Polaroid of the mirrored hall of Golestan Palace lit by large chandeliers in Tehran, Iran
Polaroid of the bronze statue of Leonidas raising a spear at the Thermopylae monument, Greece
Polaroid of a man looking up at the wooden Trojan Horse from the Troy film in Canakkale, Turkey
Polaroid of a backgammon board on a table overlooking the sea at a campsite in Turkey
Polaroid of a Soviet-era mosaic of a woman in traditional dress under a Ukrainian coat of arms in Ukraine
Polaroid of a motorcycle parked at a bus stop with a mosaic of three knights in Ukraine
Polaroid of a man standing on a boardwalk in front of the stairway of the Ziggurat of Ur, Iraq
Polaroid looking up the brick stairway of the Ziggurat of Ur, Iraq
Polaroid of eroded mud-brick ruins with a small sign in front at Uruk, Iraq
Polaroid of a man and two boys crouching by a freshly slaughtered sheep on grass on the road to Van, Turkey
Polaroid looking up at the curved glass Zlota 44 skyscraper in Warsaw, Poland
Polaroid of the Palace of Culture and Science lit at dusk in Warsaw, Poland
Polaroid of the Zoroastrian Fire Temple with a round reflecting pool in Yazd, Iran