Skip to content

Commit

Permalink
Merge branch 'master' into feature/paths-funnels-followups
Browse files Browse the repository at this point in the history
# Conflicts:
#	posthog/hogql_queries/legacy_compatibility/filter_to_query.py
  • Loading branch information
thmsobrmlr committed Mar 28, 2024
2 parents d47bd8c + 205376b commit 8d60b20
Show file tree
Hide file tree
Showing 527 changed files with 10,787 additions and 5,274 deletions.
8 changes: 0 additions & 8 deletions .github/workflows/ci-backend-depot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@
name: Backend CI (depot)

on:
push:
branches:
- master
pull_request:
workflow_dispatch:
inputs:
clickhouseServerVersion:
description: ClickHouse server version. Leave blank for default
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ci-hobby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ jobs:
token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }}
- name: Get python deps
run: pip install python-digitalocean==1.17.0 requests==2.28.1
- name: Setup DO Hobby Instance
run: python3 bin/hobby-ci.py create
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
- name: Run smoke tests on DO
run: python3 bin/hobby-ci.py $GITHUB_HEAD_REF
run: python3 bin/hobby-ci.py test $GITHUB_HEAD_REF
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
- name: Post-cleanup step
if: always()
run: python3 bin/hobby-ci.py destroy
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
4 changes: 4 additions & 0 deletions bin/docker-worker-celery
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ FLAGS+=("-n node@%h")
# On Heroku $WEB_CONCURRENCY contains suggested number of forks per dyno type
# https://github.com/heroku/heroku-buildpack-python/blob/main/vendor/WEB_CONCURRENCY.sh
[[ -n "${WEB_CONCURRENCY}" ]] && FLAGS+=" --concurrency $WEB_CONCURRENCY"
# Restart worker process after it processes this many tasks (to mitigate memory leaks)
[[ -n "${CELERY_MAX_TASKS_PER_CHILD}" ]] && FLAGS+=" --max-tasks-per-child $CELERY_MAX_TASKS_PER_CHILD"
# Restart worker process after it exceeds this much memory usage (to mitigate memory leaks)
[[ -n "${CELERY_MAX_MEMORY_PER_CHILD}" ]] && FLAGS+=" --max-memory-per-child $CELERY_MAX_MEMORY_PER_CHILD"

if [[ -z "${CELERY_WORKER_QUEUES}" ]]; then
source ./bin/celery-queues.env
Expand Down
244 changes: 170 additions & 74 deletions bin/hobby-ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,80 @@
import datetime
import os
import random
import re
import signal
import string
import sys
import time

import digitalocean
import requests

letters = string.ascii_lowercase
random_bit = "".join(random.choice(letters) for i in range(4))
name = f"do-ci-hobby-deploy-{random_bit}"
region = "sfo3"
image = "ubuntu-22-04-x64"
size = "s-4vcpu-8gb"
release_tag = "latest-release"
branch_regex = re.compile("release-*.*")
branch = sys.argv[1]
if branch_regex.match(branch):
release_tag = f"{branch}-unstable"
hostname = f"{name}.posthog.cc"
user_data = (
f"#!/bin/bash \n"
"mkdir hobby \n"
"cd hobby \n"
"sed -i \"s/#\\$nrconf{restart} = 'i';/\\$nrconf{restart} = 'a';/g\" /etc/needrestart/needrestart.conf \n"
"git clone https://github.com/PostHog/posthog.git \n"
"cd posthog \n"
f"git checkout {branch} \n"
"cd .. \n"
f"chmod +x posthog/bin/deploy-hobby \n"
f"./posthog/bin/deploy-hobby {release_tag} {hostname} 1 \n"
)
token = os.getenv("DIGITALOCEAN_TOKEN")

DOMAIN = "posthog.cc"


class HobbyTester:
def __init__(self, domain, droplet, record):
# Placeholders for DO resources
def __init__(
self,
token=None,
name=None,
region="sfo3",
image="ubuntu-22-04-x64",
size="s-4vcpu-8gb",
release_tag="latest-release",
branch=None,
hostname=None,
domain=DOMAIN,
droplet_id=None,
droplet=None,
record_id=None,
record=None,
):
if not token:
token = os.getenv("DIGITALOCEAN_TOKEN")
self.token = token
self.branch = branch
self.release_tag = release_tag

random_bit = "".join(random.choice(string.ascii_lowercase) for i in range(4))

if not name:
name = f"do-ci-hobby-deploy-{self.release_tag}-{random_bit}"
self.name = name

if not hostname:
hostname = f"{name}.{DOMAIN}"
self.hostname = hostname

self.region = region
self.image = image
self.size = size

self.domain = domain
self.droplet = droplet
if droplet_id:
self.droplet = digitalocean.Droplet(token=self.token, id=droplet_id)

self.record = record
if record_id:
self.record = digitalocean.Record(token=self.token, id=record_id)

@staticmethod
def block_until_droplet_is_started(droplet):
actions = droplet.get_actions()
self.user_data = (
f"#!/bin/bash \n"
"mkdir hobby \n"
"cd hobby \n"
"sed -i \"s/#\\$nrconf{restart} = 'i';/\\$nrconf{restart} = 'a';/g\" /etc/needrestart/needrestart.conf \n"
"git clone https://github.com/PostHog/posthog.git \n"
"cd posthog \n"
f"git checkout {self.branch} \n"
"cd .. \n"
f"chmod +x posthog/bin/deploy-hobby \n"
f"./posthog/bin/deploy-hobby {self.release_tag} {self.hostname} 1 \n"
)

def block_until_droplet_is_started(self):
if not self.droplet:
return
actions = self.droplet.get_actions()
up = False
while not up:
for action in actions:
Expand All @@ -60,42 +88,43 @@ def block_until_droplet_is_started(droplet):
print("Droplet not booted yet - waiting a bit")
time.sleep(5)

@staticmethod
def get_public_ip(droplet):
def get_public_ip(self):
if not self.droplet:
return
ip = None
while not ip:
time.sleep(1)
droplet.load()
ip = droplet.ip_address
self.droplet.load()
ip = self.droplet.ip_address
print(f"Public IP found: {ip}") # type: ignore
return ip

@staticmethod
def create_droplet(ssh_enabled=False):
def create_droplet(self, ssh_enabled=False):
keys = None
if ssh_enabled:
manager = digitalocean.Manager(token=token)
manager = digitalocean.Manager(token=self.token)
keys = manager.get_all_sshkeys()
droplet = digitalocean.Droplet(
token=token,
name=name,
region=region,
image=image,
size_slug=size,
user_data=user_data,
self.droplet = digitalocean.Droplet(
token=self.token,
name=self.name,
region=self.region,
image=self.image,
size_slug=self.size,
user_data=self.user_data,
ssh_keys=keys,
tags=["ci"],
)
droplet.create()
return droplet
self.droplet.create()
return self.droplet

@staticmethod
def wait_for_instance(hostname, timeout=20, retry_interval=15):
def test_deployment(self, timeout=20, retry_interval=15):
if not self.hostname:
return
# timeout in minutes
# return true if success or false if failure
print("Attempting to reach the instance")
print(f"We will time out after {timeout} minutes")
url = f"https://{hostname}/_health"
url = f"https://{self.hostname}/_health"
start_time = datetime.datetime.now()
while datetime.datetime.now() < start_time + datetime.timedelta(minutes=timeout):
try:
Expand All @@ -115,9 +144,29 @@ def wait_for_instance(hostname, timeout=20, retry_interval=15):
print("Failure - we timed out before receiving a heartbeat")
return False

def create_dns_entry(self, type, name, data, ttl=30):
self.domain = digitalocean.Domain(token=self.token, name=DOMAIN)
self.record = self.domain.create_new_domain_record(type=type, name=name, data=data, ttl=ttl)
return self.record

def create_dns_entry_for_instance(self):
if not self.droplet:
return
self.record = self.create_dns_entry(type="A", name=self.name, data=self.get_public_ip())
return self.record

def destroy_self(self, retries=3):
if not self.droplet or not self.domain or not self.record:
return
droplet_id = self.droplet.id
self.destroy_environment(droplet_id, self.domain, self.record["domain_record"]["id"], retries=retries)

@staticmethod
def destroy_environment(droplet, domain, record, retries=3):
def destroy_environment(droplet_id, record_id, retries=3):
print("Destroying the droplet")
token = os.getenv("DIGITALOCEAN_TOKEN")
droplet = digitalocean.Droplet(token=token, id=droplet_id)
domain = digitalocean.Domain(token=token, name=DOMAIN)
attempts = 0
while attempts <= retries:
attempts += 1
Expand All @@ -131,36 +180,83 @@ def destroy_environment(droplet, domain, record, retries=3):
while attempts <= retries:
attempts += 1
try:
domain.delete_domain_record(id=record["domain_record"]["id"])
domain.delete_domain_record(id=record_id)
break
except Exception as e:
print(f"Could not destroy the dns entry because\n{e}")

def handle_sigint(self):
self.destroy_environment(self.droplet, self.domain, self.record)
self.destroy_self()

def export_droplet(self):
if not self.droplet:
print("Droplet not found. Exiting")
exit(1)
if not self.record:
print("DNS record not found. Exiting")
exit(1)
record_id = self.record["domain_record"]["id"]
record_name = self.record["domain_record"]["name"]
droplet_id = self.droplet.id

print(f"Exporting the droplet ID: {self.droplet.id} and DNS record ID: {record_id} for name {self.name}")
env_file_name = os.getenv("GITHUB_ENV")
with open(env_file_name, "a") as env_file:
env_file.write(f"HOBBY_DROPLET_ID={droplet_id}\n")
with open(env_file_name, "a") as env_file:
env_file.write(f"HOBBY_DNS_RECORD_ID={record_id}\n")
env_file.write(f"HOBBY_DNS_RECORD_NAME={record_name}\n")
env_file.write(f"HOBBY_NAME={self.name}\n")

def ensure_droplet(self, ssh_enabled=True):
self.create_droplet(ssh_enabled=ssh_enabled)
self.block_until_droplet_is_started()
self.create_dns_entry_for_instance()
self.export_droplet()


def main():
print("Creating droplet on Digitalocean for testing Hobby Deployment")
droplet = HobbyTester.create_droplet(ssh_enabled=True)
HobbyTester.block_until_droplet_is_started(droplet)
public_ip = HobbyTester.get_public_ip(droplet)
domain = digitalocean.Domain(token=token, name="posthog.cc")
record = domain.create_new_domain_record(type="A", name=name, data=public_ip)

hobby_tester = HobbyTester(domain, droplet, record)
signal.signal(signal.SIGINT, hobby_tester.handle_sigint) # type: ignore
signal.signal(signal.SIGHUP, hobby_tester.handle_sigint) # type: ignore
print("Instance has started. You will be able to access it here after PostHog boots (~15 minutes):")
print(f"https://{hostname}")
health_success = HobbyTester.wait_for_instance(hostname)
HobbyTester.destroy_environment(droplet, domain, record)
if health_success:
print("We succeeded")
exit()
else:
print("We failed")
exit(1)
command = sys.argv[1]
if command == "create":
print("Creating droplet on Digitalocean for testing Hobby Deployment")
ht = HobbyTester()
ht.ensure_droplet(ssh_enabled=True)
print("Instance has started. You will be able to access it here after PostHog boots (~15 minutes):")
print(f"https://{ht.hostname}")

if command == "destroy":
print("Destroying droplet on Digitalocean for testing Hobby Deployment")
droplet_id = os.environ.get("HOBBY_DROPLET_ID")
domain_record_id = os.environ.get("HOBBY_DNS_RECORD_ID")
print(f"Droplet ID: {droplet_id}")
print(f"Record ID: {domain_record_id}")
HobbyTester.destroy_environment(droplet_id=droplet_id, record_id=domain_record_id)

if command == "test":
if len(sys.argv) < 3:
print("Please provide the branch name to test")
exit(1)
branch = sys.argv[2]
name = os.environ.get("HOBBY_NAME")
record_id = os.environ.get("HOBBY_DNS_RECORD_ID")
droplet_id = os.environ.get("HOBBY_DROPLET_ID")
print(f"Testing the deployment for {name} on branch {branch}")
print(f"Record ID: {record_id}")
print(f"Droplet ID: {droplet_id}")

ht = HobbyTester(
branch=branch,
name=name,
record_id=record_id,
droplet_id=droplet_id,
)
health_success = ht.test_deployment()
if health_success:
print("We succeeded")
exit()
else:
print("We failed")
exit(1)


if __name__ == "__main__":
Expand Down
35 changes: 35 additions & 0 deletions cypress/e2e/before-onboarding.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
describe('Before Onboarding', () => {
before(() => {
cy.request({
method: 'PATCH',
url: '/api/projects/1/',
body: { completed_snippet_onboarding: false },
headers: { Authorization: 'Bearer e2e_demo_api_key' },
})
})

after(() => {
cy.request({
method: 'PATCH',
url: '/api/projects/1/',
body: { completed_snippet_onboarding: true },
headers: { Authorization: 'Bearer e2e_demo_api_key' },
})
})

it('Navigate to /products when a product has not been set up', () => {
cy.visit('/project/1/data-management/events')

cy.get('[data-attr=top-bar-name] > span').contains('Products')
})

it('Navigate to a settings page even when a product has not been set up', () => {
cy.visit('/settings/user')

cy.get('[data-attr=top-bar-name] > span').contains('User')

cy.visit('/settings/organization')

cy.get('[data-attr=top-bar-name] > span').contains('Organization')
})
})
Loading

0 comments on commit 8d60b20

Please sign in to comment.