def preflight_check(request: HttpRequest) -> JsonResponse: response = { "django": True, "redis": is_redis_alive() or settings.TEST, "plugins": is_plugin_server_alive() or settings.TEST, "celery": is_celery_alive() or settings.TEST, "db": is_postgres_alive(), "initiated": Organization.objects.exists(), "cloud": settings.MULTI_TENANCY, "demo": settings.DEMO, "realm": get_instance_realm(), "available_social_auth_providers": get_available_social_auth_providers(), "can_create_org": get_can_create_org(), "email_service_available": is_email_available(with_absolute_urls=True), } if request.user.is_authenticated: response = { **response, "db_backend": settings.PRIMARY_DB.value, "available_timezones": get_available_timezones_with_offsets(), "opt_out_capture": os.environ.get("OPT_OUT_CAPTURE", False), "posthog_version": VERSION, "is_debug": settings.DEBUG, "is_event_property_usage_enabled": settings.ASYNC_EVENT_PROPERTY_USAGE, "licensed_users_available": get_licensed_users_available(), "site_url": settings.SITE_URL, "instance_preferences": settings.INSTANCE_PREFERENCES, } return JsonResponse(response)
def report_user_signed_up( distinct_id: str, is_instance_first_user: bool, is_organization_first_user: bool, new_onboarding_enabled: bool = False, backend_processor: str = "", # which serializer/view processed the request social_provider: str = "", # which third-party provider processed the login (empty = no third-party) ) -> None: """ Reports that a new user has joined. Only triggered when a new user is actually created (i.e. when an existing user joins a new organization, this event is **not** triggered; see `report_user_joined_organization`). """ props = { "is_first_user": is_instance_first_user, "is_organization_first_user": is_organization_first_user, "new_onboarding_enabled": new_onboarding_enabled, "signup_backend_processor": backend_processor, "signup_social_provider": social_provider, "realm": get_instance_realm(), } # TODO: This should be $set_once as user props. posthoganalytics.identify(distinct_id, props) posthoganalytics.capture(distinct_id, "user signed up", properties=props)
def get_analytics_metadata(self): team_member_count_all: int = (OrganizationMembership.objects.filter( organization__in=self.organizations.all(), ).values( "user_id").distinct().count()) project_setup_complete = False if self.team and self.team.completed_snippet_onboarding and self.team.ingested_event: project_setup_complete = True return { "realm": get_instance_realm(), "is_ee_available": settings.EE_AVAILABLE, "email_opt_in": self.email_opt_in, "anonymize_data": self.anonymize_data, "email": self.email if not self.anonymize_data else None, "is_signed_up": True, "organization_count": self.organization_memberships.count(), "project_count": self.teams.count(), "team_member_count_all": team_member_count_all, "completed_onboarding_once": self.teams.filter( completed_snippet_onboarding=True, ingested_event=True, ).exists( ), # has completed the onboarding at least for one project # properties dependent on current project / org below "billing_plan": self.organization.billing_plan if self.organization else None, "organization_id": str(self.organization.id) if self.organization else None, "project_id": str(self.team.uuid) if self.team else None, "project_setup_complete": project_setup_complete, "joined_at": self.date_joined, "has_password_set": self.has_usable_password(), "has_social_auth": self.social_auth.exists(), # type: ignore "social_providers": list(self.social_auth.values_list("provider", flat=True)), # type: ignore }
def test_api_invite_sign_up(self, mock_capture): invite: OrganizationInvite = OrganizationInvite.objects.create( target_email="*****@*****.**", organization=self.organization, ) response = self.client.post( f"/api/signup/{invite.id}/", {"first_name": "Alice", "password": "******", "email_opt_in": True}, ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = cast(User, User.objects.order_by("-pk")[0]) self.assertEqual( response.json(), { "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, "first_name": "Alice", "email": "*****@*****.**", }, ) # User is now a member of the organization self.assertEqual(user.organization_memberships.count(), 1) self.assertEqual(user.organization_memberships.first().organization, self.organization) # type: ignore # Defaults are set correctly self.assertEqual(user.organization, self.organization) self.assertEqual(user.team, self.team) # Assert that the user was properly created self.assertEqual(user.first_name, "Alice") self.assertEqual(user.email, "*****@*****.**") self.assertEqual(user.email_opt_in, True) # Assert that the sign up event & identify calls were sent to PostHog analytics mock_capture.assert_called_once() self.assertEqual(user.distinct_id, mock_capture.call_args.args[0]) self.assertEqual("user signed up", mock_capture.call_args.args[1]) # Assert that key properties were set properly event_props = mock_capture.call_args.kwargs["properties"] self.assertEqual(event_props["is_first_user"], False) self.assertEqual(event_props["is_organization_first_user"], False) self.assertEqual(event_props["new_onboarding_enabled"], False) self.assertEqual(event_props["signup_backend_processor"], "OrganizationInviteSignupSerializer") self.assertEqual(event_props["signup_social_provider"], "") self.assertEqual(event_props["realm"], get_instance_realm()) # Assert that the user is logged in response = self.client.get("/api/users/@me/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["email"], "*****@*****.**") # Assert that the password was correctly saved self.assertTrue(user.check_password("test_password"))
def test_signup_minimum_attrs(self, mock_identify, mock_capture): response = self.client.post( "/api/signup/", { "first_name": "Jane", "email": "*****@*****.**", "password": "******" }, ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = cast(User, User.objects.order_by("-pk").get()) organization = cast(Organization, user.organization) self.assertEqual( response.json(), { "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, "first_name": "Jane", "email": "*****@*****.**", "redirect_url": "/ingestion", }, ) # Assert that the user & org were properly created self.assertEqual(user.first_name, "Jane") self.assertEqual(user.email, "*****@*****.**") self.assertEqual(user.email_opt_in, True) # Defaults to True self.assertEqual(organization.name, "Jane") # Assert that the sign up event & identify calls were sent to PostHog analytics mock_identify.assert_called_once() mock_capture.assert_called_once_with( user.distinct_id, "user signed up", properties={ "is_first_user": True, "is_organization_first_user": True, "new_onboarding_enabled": False, "signup_backend_processor": "OrganizationSignupSerializer", "signup_social_provider": "", "realm": get_instance_realm(), }, ) # Assert that the user is logged in response = self.client.get("/api/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["email"], "*****@*****.**") # Assert that the password was correctly saved self.assertTrue(user.check_password("notsecure"))
def report_user_signed_up( user: User, is_instance_first_user: bool, is_organization_first_user: bool, new_onboarding_enabled: bool = False, backend_processor: str = "", # which serializer/view processed the request social_provider: str = "", # which third-party provider processed the login (empty = no third-party) user_analytics_metadata: Optional[ dict] = None, # analytics metadata taken from the User object org_analytics_metadata: Optional[ dict] = None, # analytics metadata taken from the Organization object ) -> None: """ Reports that a new user has joined. Only triggered when a new user is actually created (i.e. when an existing user joins a new organization, this event is **not** triggered; see `report_user_joined_organization`). """ props = { "is_first_user": is_instance_first_user, "is_organization_first_user": is_organization_first_user, "new_onboarding_enabled": new_onboarding_enabled, "signup_backend_processor": backend_processor, "signup_social_provider": social_provider, "realm": get_instance_realm(), } if user_analytics_metadata is not None: props.update(user_analytics_metadata) if org_analytics_metadata is not None: for k, v in org_analytics_metadata.items(): props[f"org__{k}"] = v # TODO: This should be $set_once as user props. posthoganalytics.identify(user.distinct_id, props) posthoganalytics.capture( user.distinct_id, "user signed up", properties=props, groups=groups(user.organization, user.team), )
def user(request): """ DEPRECATED: This endpoint (/api/user/) has been deprecated in favor of /api/users/@me/ and will be removed soon. """ organization: Optional[Organization] = request.user.organization organizations = list( request.user.organizations.order_by("-created_at").values( "name", "id")) team: Optional[Team] = request.user.team teams = list( request.user.teams.order_by("-created_at").values("name", "id")) user = cast(User, request.user) if request.method == "PATCH": data = json.loads(request.body) if team is not None and "team" in data: team.app_urls = data["team"].get("app_urls", team.app_urls) team.slack_incoming_webhook = data["team"].get( "slack_incoming_webhook", team.slack_incoming_webhook) team.anonymize_ips = data["team"].get("anonymize_ips", team.anonymize_ips) team.session_recording_opt_in = data["team"].get( "session_recording_opt_in", team.session_recording_opt_in) team.session_recording_retention_period_days = data["team"].get( "session_recording_retention_period_days", team.session_recording_retention_period_days, ) team.completed_snippet_onboarding = data["team"].get( "completed_snippet_onboarding", team.completed_snippet_onboarding, ) team.test_account_filters = data["team"].get( "test_account_filters", team.test_account_filters) team.timezone = data["team"].get("timezone", team.timezone) team.save() if "user" in data: try: user.current_organization = user.organizations.get( id=data["user"]["current_organization_id"]) assert user.organization is not None, "Organization should have been just set" user.current_team = user.organization.teams.first() except (KeyError, ValueError): pass except ObjectDoesNotExist: return JsonResponse( {"detail": "Organization not found for user."}, status=404) except KeyError: pass except ObjectDoesNotExist: return JsonResponse( {"detail": "Organization not found for user."}, status=404) if user.organization is not None: try: user.current_team = user.organization.teams.get( id=int(data["user"]["current_team_id"])) except (KeyError, TypeError): pass except ValueError: return JsonResponse( {"detail": "Team ID must be an integer."}, status=400) except ObjectDoesNotExist: return JsonResponse( { "detail": "Team not found for user's current organization." }, status=404) user.email_opt_in = data["user"].get("email_opt_in", user.email_opt_in) user.anonymize_data = data["user"].get("anonymize_data", user.anonymize_data) user.toolbar_mode = data["user"].get("toolbar_mode", user.toolbar_mode) user.save() user_identify.identify_task.delay(user_id=user.id) return JsonResponse({ "deprecation": "Endpoint has been deprecated. Please use `/api/users/@me/`.", "id": user.pk, "distinct_id": user.distinct_id, "name": user.first_name, "email": user.email, "email_opt_in": user.email_opt_in, "anonymize_data": user.anonymize_data, "toolbar_mode": user.toolbar_mode, "organization": None if organization is None else { "id": organization.id, "name": organization.name, "billing_plan": organization.billing_plan, "available_features": organization.available_features, "plugins_access_level": organization.plugins_access_level, "created_at": organization.created_at, "updated_at": organization.updated_at, "teams": [{ "id": team.id, "name": team.name } for team in organization.teams.all().only("id", "name")], }, "organizations": organizations, "team": None if team is None else { "id": team.id, "name": team.name, "app_urls": team.app_urls, "api_token": team.api_token, "anonymize_ips": team.anonymize_ips, "slack_incoming_webhook": team.slack_incoming_webhook, "completed_snippet_onboarding": team.completed_snippet_onboarding, "session_recording_opt_in": team.session_recording_opt_in, "session_recording_retention_period_days": team.session_recording_retention_period_days, "ingested_event": team.ingested_event, "is_demo": team.is_demo, "test_account_filters": team.test_account_filters, "timezone": team.timezone, "data_attributes": team.data_attributes, }, "teams": teams, "has_password": user.has_usable_password(), "opt_out_capture": os.environ.get("OPT_OUT_CAPTURE"), "posthog_version": VERSION, "is_multi_tenancy": getattr(settings, "MULTI_TENANCY", False), "ee_available": settings.EE_AVAILABLE, "is_clickhouse_enabled": is_clickhouse_enabled(), "email_service_available": is_email_available(with_absolute_urls=True), "is_debug": getattr(settings, "DEBUG", False), "is_staff": user.is_staff, "is_impersonated": is_impersonated_session(request), "is_event_property_usage_enabled": getattr(settings, "ASYNC_EVENT_PROPERTY_USAGE", False), "realm": get_instance_realm(), })
def status_report(*, dry_run: bool = False) -> Dict[str, Any]: period_start, period_end = get_previous_week() report: Dict[str, Any] = { "posthog_version": VERSION, "clickhouse_version": str(version_requirement.get_clickhouse_version()), "deployment": os.getenv("DEPLOYMENT", "unknown"), "realm": get_instance_realm(), "period": { "start_inclusive": period_start.isoformat(), "end_inclusive": period_end.isoformat() }, "site_url": os.getenv("SITE_URL", "unknown"), "license_keys": get_instance_licenses(), } report["helm"] = get_helm_info_env() report["users_who_logged_in"] = [{ "id": user.id, "distinct_id": user.distinct_id } if user.anonymize_data else { "id": user.id, "distinct_id": user.distinct_id, "first_name": user.first_name, "email": user.email } for user in User.objects.filter(is_active=True, last_login__gte=period_start)] report["teams"] = {} report["table_sizes"] = { "posthog_event": fetch_table_size("posthog_event"), "posthog_sessionrecordingevent": fetch_table_size("posthog_sessionrecordingevent"), } plugin_configs = PluginConfig.objects.select_related("plugin").all() report["plugins_installed"] = Counter(plugin_config.plugin.name for plugin_config in plugin_configs) report["plugins_enabled"] = Counter(plugin_config.plugin.name for plugin_config in plugin_configs if plugin_config.enabled) instance_usage_summary: Dict[str, int] = { "events_count_new_in_period": 0, "persons_count_new_in_period": 0, "persons_count_total": 0, "events_count_total": 0, "dashboards_count": 0, "ff_count": 0, "using_groups": False, } for team in Team.objects.exclude(organization__for_internal_metrics=True): try: params = (team.id, report["period"]["start_inclusive"], report["period"]["end_inclusive"]) team_report: Dict[str, Any] = {} # pull events stats from clickhouse from ee.clickhouse.models.event import ( get_event_count_for_team, get_event_count_for_team_and_period, get_events_count_for_team_by_client_lib, get_events_count_for_team_by_event_type, ) from ee.clickhouse.models.person import ( count_duplicate_distinct_ids_for_team, count_total_persons_with_multiple_ids, ) team_event_count = get_event_count_for_team(team.id) instance_usage_summary["events_count_total"] += team_event_count team_report["events_count_total"] = team_event_count team_events_in_period_count = get_event_count_for_team_and_period( team.id, period_start, period_end) team_report[ "events_count_new_in_period"] = team_events_in_period_count instance_usage_summary[ "events_count_new_in_period"] += team_report[ "events_count_new_in_period"] team_report[ "events_count_by_lib"] = get_events_count_for_team_by_client_lib( team.id, period_start, period_end) team_report[ "events_count_by_name"] = get_events_count_for_team_by_event_type( team.id, period_start, period_end) team_report[ "duplicate_distinct_ids"] = count_duplicate_distinct_ids_for_team( team.id) team_report[ "multiple_ids_per_person"] = count_total_persons_with_multiple_ids( team.id) team_report["group_types_count"] = GroupTypeMapping.objects.filter( team_id=team.id).count() if team_report["group_types_count"] > 0: instance_usage_summary["using_groups"] = True # pull person stats and the rest here from Postgres always persons_considered_total = Person.objects.filter(team_id=team.id) persons_considered_total_new_in_period = persons_considered_total.filter( created_at__gte=period_start, created_at__lte=period_end, ) team_report[ "persons_count_total"] = persons_considered_total.count() instance_usage_summary["persons_count_total"] += team_report[ "persons_count_total"] team_report[ "persons_count_new_in_period"] = persons_considered_total_new_in_period.count( ) instance_usage_summary[ "persons_count_new_in_period"] += team_report[ "persons_count_new_in_period"] # Dashboards team_dashboards = Dashboard.objects.filter(team=team).exclude( deleted=True) team_report["dashboards_count"] = team_dashboards.count() instance_usage_summary["dashboards_count"] += team_report[ "dashboards_count"] team_report["dashboards_template_count"] = team_dashboards.filter( creation_mode="template").count() team_report["dashboards_shared_count"] = team_dashboards.filter( is_shared=True).count() team_report["dashboards_tagged_count"] = team_dashboards.exclude( tagged_items__isnull=True).count() # Feature Flags feature_flags = FeatureFlag.objects.filter(team=team).exclude( deleted=True) team_report["ff_count"] = feature_flags.count() instance_usage_summary["ff_count"] += team_report["ff_count"] team_report["ff_active_count"] = feature_flags.filter( active=True).count() report["teams"][team.id] = team_report except Exception as err: capture_event("instance status report failure", {"error": str(err)}, dry_run=dry_run) report["instance_usage_summary"] = instance_usage_summary capture_event("instance status report", report, dry_run=dry_run) return report
def send_all_reports(*, dry_run: bool = False) -> List[OrgReport]: """ Generic way to generate and send org usage reports. Specify Postgres or ClickHouse for event queries. """ period_start, period_end = get_previous_day() realm = get_instance_realm() license_keys = get_instance_licenses() metadata: OrgReportMetadata = { "posthog_version": VERSION, "deployment_infrastructure": os.getenv("DEPLOYMENT", "unknown"), "realm": realm, "period": { "start_inclusive": period_start.isoformat(), "end_inclusive": period_end.isoformat() }, "site_url": os.getenv("SITE_URL", "unknown"), "license_keys": license_keys, "product": get_product_name(realm, license_keys), } org_data: Dict[str, OrgData] = {} org_reports: List[OrgReport] = [] for team in Team.objects.exclude(organization__for_internal_metrics=True): org = team.organization organization_id = str(org.id) if organization_id in org_data: org_data[organization_id]["teams"].append(team.id) else: org_data[organization_id] = { "teams": [team.id], "user_count": get_org_user_count(organization_id), "name": org.name, "created_at": str(org.created_at), } for organization_id, org in org_data.items(): org_owner = get_org_owner_or_first_user(organization_id) if not org_owner: continue distinct_id = org_owner.distinct_id try: month_start = period_start.replace(day=1) usage = get_org_usage( team_ids=org["teams"], period_start=period_start, period_end=period_end, month_start=month_start, ) report: dict = { **metadata, **usage, "organization_id": organization_id, "organization_name": org["name"], "organization_created_at": org["created_at"], "organization_user_count": org["user_count"], "team_count": len(org["teams"]), } org_reports.append(report) # type: ignore except Exception as err: logger.warning("Organization usage report calculation failed", err) if not dry_run: report_org_usage_failure(organization_id, distinct_id, str(err)) if not dry_run: report_org_usage(organization_id, distinct_id, report) time.sleep(0.25) return org_reports
def status_report(*, dry_run: bool = False) -> Dict[str, Any]: period_start, period_end = get_previous_week() report: Dict[str, Any] = { "posthog_version": VERSION, "deployment": os.getenv("DEPLOYMENT", "unknown"), "realm": get_instance_realm(), "period": { "start_inclusive": period_start.isoformat(), "end_inclusive": period_end.isoformat() }, "site_url": os.getenv("SITE_URL", "unknown"), } report["helm"] = get_helm_info_env() report["users_who_logged_in"] = [{ "id": user.id, "distinct_id": user.distinct_id } if user.anonymize_data else { "id": user.id, "distinct_id": user.distinct_id, "first_name": user.first_name, "email": user.email } for user in User.objects.filter(last_login__gte=period_start)] report["teams"] = {} report["table_sizes"] = { "posthog_event": fetch_table_size("posthog_event"), "posthog_sessionrecordingevent": fetch_table_size("posthog_sessionrecordingevent"), } plugin_configs = PluginConfig.objects.select_related("plugin").all() report["plugins_installed"] = Counter( (plugin_config.plugin.name for plugin_config in plugin_configs)) report["plugins_enabled"] = Counter((plugin_config.plugin.name for plugin_config in plugin_configs if plugin_config.enabled)) instance_usage_summary: Dict[str, int] = { "events_count_new_in_period": 0, "persons_count_new_in_period": 0, "persons_count_total": 0, "events_count_total": 0, "dashboards_count": 0, "ff_count": 0, } for team in Team.objects.exclude(organization__for_internal_metrics=True): try: team_report: Dict[str, Any] = {} events_considered_total = Event.objects.filter(team_id=team.id) instance_usage_summary[ "events_count_total"] += events_considered_total.count() events_considered_new_in_period = events_considered_total.filter( timestamp__gte=period_start, timestamp__lte=period_end, ) persons_considered_total = Person.objects.filter(team_id=team.id) persons_considered_total_new_in_period = persons_considered_total.filter( created_at__gte=period_start, created_at__lte=period_end, ) team_report["events_count_total"] = events_considered_total.count() team_report[ "events_count_new_in_period"] = events_considered_new_in_period.count( ) instance_usage_summary[ "events_count_new_in_period"] += team_report[ "events_count_new_in_period"] team_report[ "persons_count_total"] = persons_considered_total.count() instance_usage_summary["persons_count_total"] += team_report[ "persons_count_total"] team_report[ "persons_count_new_in_period"] = persons_considered_total_new_in_period.count( ) instance_usage_summary[ "persons_count_new_in_period"] += team_report[ "persons_count_new_in_period"] params = (team.id, report["period"]["start_inclusive"], report["period"]["end_inclusive"]) team_report[ "persons_count_active_in_period"] = fetch_persons_count_active_in_period( params) team_report["events_count_by_lib"] = fetch_event_counts_by_lib( params) team_report["events_count_by_name"] = fetch_events_count_by_name( params) # Dashboards team_dashboards = Dashboard.objects.filter(team=team).exclude( deleted=True) team_report["dashboards_count"] = team_dashboards.count() instance_usage_summary["dashboards_count"] += team_report[ "dashboards_count"] team_report["dashboards_template_count"] = team_dashboards.filter( creation_mode="template").count() team_report["dashboards_shared_count"] = team_dashboards.filter( is_shared=True).count() team_report["dashboards_tagged_count"] = team_dashboards.exclude( tags=[]).count() # Feature Flags feature_flags = FeatureFlag.objects.filter(team=team).exclude( deleted=True) team_report["ff_count"] = feature_flags.count() instance_usage_summary["ff_count"] += team_report["ff_count"] team_report["ff_active_count"] = feature_flags.filter( active=True).count() report["teams"][team.id] = team_report except Exception as err: capture_event("instance status report failure", {"error": str(err)}, dry_run=dry_run) report["instance_usage_summary"] = instance_usage_summary capture_event("instance status report", report, dry_run=dry_run) return report
def test_api_sign_up(self, mock_capture): # Ensure the internal system metrics org doesn't prevent org-creation Organization.objects.create(name="PostHog Internal Metrics", for_internal_metrics=True) response = self.client.post( "/api/signup/", { "first_name": "John", "email": "*****@*****.**", "password": "******", "organization_name": "Hedgehogs United, LLC", "email_opt_in": False, }, ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = cast(User, User.objects.order_by("-pk")[0]) team = cast(Team, user.team) organization = cast(Organization, user.organization) self.assertEqual( response.json(), { "id": user.pk, "uuid": str(user.uuid), "distinct_id": user.distinct_id, "first_name": "John", "email": "*****@*****.**", "redirect_url": "/ingestion", }, ) # Assert that the user was properly created self.assertEqual(user.first_name, "John") self.assertEqual(user.email, "*****@*****.**") self.assertEqual(user.email_opt_in, False) # Assert that the team was properly created self.assertEqual(team.name, "Default Project") # Assert that the org was properly created self.assertEqual(organization.name, "Hedgehogs United, LLC") # Assert that the sign up event & identify calls were sent to PostHog analytics mock_capture.assert_called_once_with( user.distinct_id, "user signed up", properties={ "is_first_user": True, "is_organization_first_user": True, "new_onboarding_enabled": False, "signup_backend_processor": "OrganizationSignupSerializer", "signup_social_provider": "", "realm": get_instance_realm(), }, ) # Assert that the user is logged in response = self.client.get("/api/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["email"], "*****@*****.**") # Assert that the password was correctly saved self.assertTrue(user.check_password("notsecure"))