Automating PostHog Annotations

How I automated adding markers to my analytics when posts go live

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 }}