def get_queryset(self): queryset = super().get_queryset() if self.action == "get" or self.action == "list": if can_install_plugins_via_api(self.organization) or can_configure_plugins_via_api(self.organization): return queryset else: if can_install_plugins_via_api(self.organization): return queryset return queryset.none()
def get_queryset(self): queryset = super().get_queryset() if self.action == "get" or self.action == "list": # type: ignore if can_install_plugins_via_api() or can_configure_plugins_via_api(): return queryset else: if can_install_plugins_via_api(): # block update/delete for plugins that come from posthog.json return queryset.filter(from_json=False) return queryset.none()
def repository(self, request: request.Request, **kwargs): if not can_install_plugins_via_api(self.organization): raise ValidationError( "Plugin installation via the web is disabled!") url = "https://raw.githubusercontent.com/PostHog/plugin-repository/main/repository.json" plugins = requests.get(url) return Response(json.loads(plugins.text))
def update(self, plugin: Plugin, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: # type: ignore if not can_install_plugins_via_api(self.context["organization_id"]): raise ValidationError("Plugin upgrades via the web are disabled!") if plugin.plugin_type != Plugin.PluginType.SOURCE: validated_data = self._update_validated_data_from_url(validated_data, validated_data["url"]) response = super().update(plugin, validated_data) reload_plugins_on_workers() return response
def update(self, plugin: Plugin, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: # type: ignore if not can_install_plugins_via_api(): raise ValidationError("Plugin upgrades via the web are disabled!") validated_data = self._get_validated_data_for_url( validated_data["url"]) response = super().update(plugin, validated_data) reload_plugins_on_workers() return response
def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: if not can_install_plugins_via_api(self.context["organization_id"]): raise ValidationError("Plugin installation via the web is disabled!") if validated_data.get("plugin_type", None) != Plugin.PluginType.SOURCE: self._update_validated_data_from_url(validated_data, validated_data["url"]) if len(Plugin.objects.filter(name=validated_data["name"])) > 0: raise ValidationError('Plugin with name "{}" already installed!'.format(validated_data["name"])) validated_data["organization_id"] = self.context["organization_id"] plugin = super().create(validated_data) reload_plugins_on_workers() return plugin
def status(self, request: request.Request, **kwargs): if not can_install_plugins_via_api(self.organization): raise ValidationError("Plugin installation via the web is disabled!") ping = get_client().get("@posthog-plugin-server/ping") if ping: ping_datetime = parser.isoparse(ping) if ping_datetime > now() - relativedelta(seconds=30): return Response({"status": "online"}) return Response({"status": "offline"})
def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: validated_data["url"] = self.initial_data.get("url", None) if not can_install_plugins_via_api(self.context["organization_id"]): raise ValidationError("Plugin installation via the web is disabled!") if validated_data.get("plugin_type", None) != Plugin.PluginType.SOURCE: self._update_validated_data_from_url(validated_data, validated_data["url"]) self._raise_if_plugin_installed(validated_data["url"]) validated_data["organization_id"] = self.context["organization_id"] plugin = super().create(validated_data) reload_plugins_on_workers() return plugin
def check_for_updates(self, request: request.Request, **kwargs): if not can_install_plugins_via_api(self.organization): raise ValidationError("Plugin installation via the web is disabled!") plugin = self.get_object() latest_url = parse_url(plugin.url, get_latest_if_none=True) plugin.latest_tag = latest_url.get("tag", latest_url.get("version", None)) plugin.latest_tag_checked_at = now() plugin.save() return Response({"plugin": PluginSerializer(plugin).data})
def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: if not can_install_plugins_via_api(): raise ValidationError( "Plugin installation via the web is disabled!") validated_data = self._get_validated_data_for_url( validated_data["url"]) if len(Plugin.objects.filter(name=validated_data["name"])) > 0: raise ValidationError( 'Plugin with name "{}" already installed!'.format( validated_data["name"])) plugin = super().create(validated_data) reload_plugins_on_workers() return plugin
def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Plugin: if not can_install_plugins_via_api(): raise ValidationError("Plugin installation via the web is disabled!") local_plugin = validated_data.get("url", "").startswith("file:") if local_plugin: plugin_path = validated_data["url"][5:] json_path = os.path.join(plugin_path, "plugin.json") json = load_json_file(json_path) if not json: raise ValidationError("Could not load plugin.json from: {}".format(json_path)) validated_data["name"] = json.get("name", json_path.split("/")[-2]) validated_data["description"] = json.get("description", "") validated_data["config_schema"] = json.get("config", {}) else: parsed_url = parse_url(validated_data["url"], get_latest_if_none=True) if parsed_url: validated_data["url"] = parsed_url["root_url"] validated_data["tag"] = parsed_url.get("version", parsed_url.get("tag", None)) validated_data["archive"] = download_plugin_archive(validated_data["url"], validated_data["tag"]) plugin_json = get_json_from_archive(validated_data["archive"], "plugin.json") if not plugin_json: raise ValidationError("Could not find plugin.json in the plugin") validated_data["name"] = plugin_json["name"] validated_data["description"] = plugin_json.get("description", "") validated_data["config_schema"] = plugin_json.get("config", {}) else: raise ValidationError("Must be a GitHub repository or a NPM package URL!") if len(Plugin.objects.filter(name=validated_data["name"])) > 0: raise ValidationError('Plugin with name "{}" already installed!'.format(validated_data["name"])) validated_data["from_web"] = True plugin = super().create(validated_data) reload_plugins_on_workers() return plugin
def get_error(self, plugin: Plugin) -> Optional[JSONField]: if plugin.error and can_install_plugins_via_api(self.context["organization_id"]): return plugin.error return None
def user(request): organization = request.user.organization organizations = list( request.user.organizations.order_by("-created_at").values( "name", "id")) 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" in data: team.app_urls = data["team"].get("app_urls", team.app_urls) team.opt_out_capture = data["team"].get("opt_out_capture", team.opt_out_capture) 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) if data["team"].get("plugins_opt_in") is not None: reload_plugins_on_workers() team.plugins_opt_in = data["team"].get("plugins_opt_in", team.plugins_opt_in) team.completed_snippet_onboarding = data["team"].get( "completed_snippet_onboarding", team.completed_snippet_onboarding, ) team.save() if "user" in data: try: user.current_organization = user.organizations.get( id=data["user"]["current_organization_id"]) user.current_team = user.organization.teams.first() except KeyError: 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) 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) posthoganalytics.identify( user.distinct_id, { "email_opt_in": user.email_opt_in, "anonymize_data": user.anonymize_data, "email": user.email if not user.anonymize_data else None, "is_signed_up": True, "toolbar_mode": user.toolbar_mode, "billing_plan": user.organization.billing_plan, "is_team_unique_user": (team.users.count() == 1), "team_setup_complete": (team.completed_snippet_onboarding and team.ingested_event), }, ) user.save() return JsonResponse({ "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": { "id": organization.id, "name": organization.name, "billing_plan": organization.billing_plan, "available_features": organization.available_features, "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": team and { "id": team.id, "name": team.name, "app_urls": team.app_urls, "api_token": team.api_token, "opt_out_capture": team.opt_out_capture, "anonymize_ips": team.anonymize_ips, "slack_incoming_webhook": team.slack_incoming_webhook, "event_names": team.event_names, "event_properties": team.event_properties, "event_properties_numerical": team.event_properties_numerical, "completed_snippet_onboarding": team.completed_snippet_onboarding, "session_recording_opt_in": team.session_recording_opt_in, "plugins_opt_in": team.plugins_opt_in, "ingested_event": team.ingested_event, }, "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": user.ee_available, "plugin_access": { "install": can_install_plugins_via_api(), "configure": can_configure_plugins_via_api() }, })
def user(request): 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.opt_out_capture = data["team"].get("opt_out_capture", team.opt_out_capture) 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) if data["team"].get("plugins_opt_in") is not None: reload_plugins_on_workers() team.plugins_opt_in = data["team"].get("plugins_opt_in", team.plugins_opt_in) team.completed_snippet_onboarding = data["team"].get( "completed_snippet_onboarding", team.completed_snippet_onboarding, ) 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({ "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, "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, "opt_out_capture": team.opt_out_capture, "anonymize_ips": team.anonymize_ips, "slack_incoming_webhook": team.slack_incoming_webhook, "event_names": team.event_names, "event_names_with_usage": team.event_names_with_usage or [{ "event": event, "volume": None, "usage_count": None } for event in team.event_names], "event_properties": team.event_properties, "event_properties_numerical": team.event_properties_numerical, "event_properties_with_usage": team.event_properties_with_usage or [{ "key": key, "volume": None, "usage_count": None } for key in team.event_properties], "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, "plugins_opt_in": team.plugins_opt_in, "ingested_event": team.ingested_event, }, "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, "ee_enabled": is_ee_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), "plugin_access": { "install": can_install_plugins_via_api(user.organization), "configure": can_configure_plugins_via_api(user.organization), }, })
def status(self, request: request.Request, **kwargs): if not can_install_plugins_via_api(self.organization): raise ValidationError( "Plugin installation via the web is disabled!") return Response( {"status": "online" if is_plugin_server_alive() else "offline"})
def system_status(request): team = request.user.team is_multitenancy: bool = getattr(settings, "MULTI_TENANCY", False) if is_multitenancy and not request.user.is_staff: raise AuthenticationFailed(detail="You're not authorized.") from .models import Element, Event, SessionRecordingEvent redis_alive = is_redis_alive() postgres_alive = is_postgres_alive() metrics: List[Dict[str, Union[str, bool, int, float]]] = [] metrics.append({ "key": "analytics_database", "metric": "Analytics database in use", "value": "ClickHouse" if is_ee_enabled() else "Postgres", }) metrics.append({ "key": "ingestion_server", "metric": "Event ingestion via", "value": "Plugin Server" if settings.PLUGIN_SERVER_INGESTION else "Django", }) metrics.append({ "key": "db_alive", "metric": "Postgres database alive", "value": postgres_alive }) if postgres_alive: postgres_version = connection.cursor().connection.server_version metrics.append({ "key": "pg_version", "metric": "Postgres version", "value": f"{postgres_version // 10000}.{(postgres_version // 100) % 100}.{postgres_version % 100}", }) event_table_count = get_table_approx_count( Event._meta.db_table)[0]["approx_count"] event_table_size = get_table_size(Event._meta.db_table)[0]["size"] element_table_count = get_table_approx_count( Element._meta.db_table)[0]["approx_count"] element_table_size = get_table_size(Element._meta.db_table)[0]["size"] session_recording_event_table_count = get_table_approx_count( SessionRecordingEvent._meta.db_table)[0]["approx_count"] session_recording_event_table_size = get_table_size( SessionRecordingEvent._meta.db_table)[0]["size"] metrics.append({ "metric": "Postgres elements table size", "value": f"~{element_table_count} rows (~{element_table_size})" }) metrics.append({ "metric": "Postgres events table size", "value": f"~{event_table_count} rows (~{event_table_size})" }) metrics.append({ "metric": "Postgres session recording table size", "value": f"~{session_recording_event_table_count} rows (~{session_recording_event_table_size})", }) metrics.append({ "key": "redis_alive", "metric": "Redis alive", "value": redis_alive }) if redis_alive: import redis try: redis_info = get_redis_info() redis_queue_depth = get_redis_queue_depth() metrics.append({ "metric": "Redis version", "value": f"{redis_info.get('redis_version')}" }) metrics.append({ "metric": "Redis current queue depth", "value": f"{redis_queue_depth}" }) metrics.append({ "metric": "Redis connected client count", "value": f"{redis_info.get('connected_clients')}" }) metrics.append({ "metric": "Redis memory used", "value": f"{redis_info.get('used_memory_human', '?')}B" }) metrics.append({ "metric": "Redis memory peak", "value": f"{redis_info.get('used_memory_peak_human', '?')}B" }) metrics.append({ "metric": "Redis total memory available", "value": f"{redis_info.get('total_system_memory_human', '?')}B", }) except redis.exceptions.ConnectionError as e: metrics.append({ "metric": "Redis metrics", "value": f"Redis connected but then failed to return metrics: {e}" }) metrics.append({ "key": "plugin_sever_alive", "metric": "Plugin server alive", "value": is_plugin_server_alive() }) metrics.append({ "key": "plugin_sever_version", "metric": "Plugin server version", "value": get_plugin_server_version() or "unknown", }) metrics.append({ "key": "plugins_install", "metric": "Plugins can be installed", "value": can_install_plugins_via_api(team.organization), }) metrics.append({ "key": "plugins_configure", "metric": "Plugins can be configured", "value": can_configure_plugins_via_api(team.organization), }) return JsonResponse({"results": metrics})
def get_error(self, plugin: Plugin) -> Optional[JSONField]: if plugin.error and can_install_plugins_via_api(): return plugin.error return None