One of the things I’m trying to get better at is using the analytics I have to my advantage. I use a tool called PostHog, which is a Google Analytics alternative, but much better and easier to understand in my opinion. PostHog supports the concept of Annotations, which are ways to define events on your data whenever “stuff” happens. With Annotations existing within the time range, they are displayed on the charts to help when something happened to help attribute that event to trends in the data. Here is what they look like:
Well last week I built a small tool in Go to automatically add these to my project in PostHog whenever a new post goes live. Here’s how I did it.
Creating Annotations with the PostHog API
The first step was to figure out how to programmatically create Annotations. PostHog has an API that can be used with a personal access token. One of the endpoints is to work with Annotations.
So once I created a token and grabbed my project ID, I wrote a function to create these Annotations using the data I wanted:
func CreateAnnotation(content string, dateString string) int {
projectId := os.Getenv("PH_PROJECT_ID")
apiKey := os.Getenv("PH_API_KEY")
jbytes, err := json.Marshal(CreateAnnotationBody{
Content: content,
DateMarker: dateString,
Scope: "project",
})
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("https://app.posthog.com/api/projects/%v/annotations/", projectId), bytes.NewReader(jbytes))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+apiKey)
c := http.Client{}
res, err := c.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
jbytes, err = ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
var responseBody CreatAnnotationResponse
err = json.Unmarshal(jbytes, &responseBody)
if err != nil {
log.Fatal(err)
}
return responseBody.Id
}
Storing the IDs with the post in Notion
Next I wanted to create these for my pre-existing posts too, so I needed a way to track what posts had Annotations created vs those that havent. I created a new field in my Posts database in Notion to store the ID of the created Annotation.
Then I updated the code to grab any post that didn’t have an Annotation associated with it, create the Annotation, and update the record in Notion.
func main() {
godotenv.Load()
c := notion.NewClient(os.Getenv("NOTION_TOKEN"))
r, err := c.QueryDatabase(context.TODO(), os.Getenv("NOTION_CMS_DBID"), ¬ion.DatabaseQuery{
Filter: ¬ion.DatabaseQueryFilter{
Property: "Status",
And: []notion.DatabaseQueryFilter{
{
Property: "Status",
DatabaseQueryPropertyFilter: notion.DatabaseQueryPropertyFilter{
Status: ¬ion.StatusDatabaseQueryFilter{
Equals: "Published",
},
},
},
{
Property: "PostHog annotation ID",
DatabaseQueryPropertyFilter: notion.DatabaseQueryPropertyFilter{
Number: ¬ion.NumberDatabaseQueryFilter{
IsEmpty: true,
},
},
},
},
},
})
if err != nil {
log.Fatal(err)
}
for _, el := range r.Results {
props := el.Properties.(notion.DatabasePageProperties)
content := fmt.Sprintf("Published \"%v\"", props["Title"].Title[0].PlainText)
dateStr := props["Publish on"].Date.Start.Time.Format(time.RFC3339)
annotationId := CreateAnnotation(content, dateStr)
fl := float64(annotationId)
updates := notion.UpdatePageParams{
DatabasePageProperties: notion.DatabasePageProperties{
"PostHog annotation ID": notion.DatabasePageProperty{
Number: &fl,
},
},
}
_, err = c.UpdatePage(context.TODO(), el.ID, updates)
if err != nil {
log.Fatal(err)
}
log.Printf("%v @ %v -- %v", content, dateStr, annotationId)
}
log.Print("done!")
}
Automating it using GitHub Actions
The final step is to automate this process. I deploy my site to Netlify, but use GitHub Actions to do it. I was able to easily add an extra step to my deploy so it would run the Go code whenever a new push to main triggered the workflow.
name: Deploy prod
on:
workflow_dispatch:
push:
branches:
- main
paths:
- website/**.*
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Netlify CLI
run: npm install -g netlify-cli
- name: Install deps
run: |
cd website
npm install
- name: Deploy
run: |
cd website
npm run deploy:prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
- name: Create PostHog annotations # <== HERE IS THE NE STEP
run: |
cd tools/posthog-annotator
go mod tidy
go run .
env:
PH_PROJECT_ID: ${{ secrets.PH_PROJECT_ID }}
PH_API_KEY: ${{ secrets.PH_API_KEY }}
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_CMS_DBID: ${{ secrets.NOTION_CMS_DBID }}