diff --git a/products/error_tracking/backend/facade/query_utils.py b/products/error_tracking/backend/facade/query_utils.py index 5ca62f60e1e3..82195cf2abad 100644 --- a/products/error_tracking/backend/facade/query_utils.py +++ b/products/error_tracking/backend/facade/query_utils.py @@ -42,20 +42,43 @@ def normalize_volume_resolution(volume_resolution: int) -> int: CONTEXT_EVENT_SELECTS = ["properties.$exception_list", "properties.$exception_releases"] -EVENT_PROPERTY_SELECTS = [ - "properties.$exception_types", - "properties.$exception_values", - "properties.$exception_list", - "properties.$exception_fingerprint", - "properties.$exception_issue_id", - "properties.$session_id", - "properties.$lib", - "properties.$browser", - "properties.$browser_version", - "properties.$os", - "properties.$os_version", - "properties.$current_url", -] +DEFAULT_EVENT_CONTEXT_INCLUDES = ["exception", "environment", "navigation", "correlation"] +EVENT_CONTEXT_PROPERTY_SELECTS = { + "exception": [ + "properties.$exception_types", + "properties.$exception_values", + "properties.$exception_list", + "properties.$exception_fingerprint", + "properties.$exception_level", + "properties.$exception_handled", + ], + "stacktrace": ["properties.$exception_list"], + "code_variables": ["properties.$exception_list"], + "environment": [ + "properties.$lib", + "properties.$lib_version", + "properties.$browser", + "properties.$browser_version", + "properties.$os", + "properties.$os_version", + "properties.$app_namespace", + "properties.$app_version", + "properties.$device_type", + ], + "release": ["properties.$exception_releases"], + "navigation": ["properties.$current_url", "properties.$screen_name", "properties.$referrer"], + "correlation": [ + "properties.$session_id", + "properties.$trace_id", + "properties.$span_id", + "properties.$ai_trace_id", + "properties.$ai_span_id", + ], + "diagnostics": ["properties.$cymbal_errors"], +} +EVENT_PROPERTY_SELECTS = list( + dict.fromkeys(select for context_group in EVENT_CONTEXT_PROPERTY_SELECTS.values() for select in context_group) +) EVENT_SELECTS = ["uuid", "timestamp", "distinct_id", *EVENT_PROPERTY_SELECTS] EVENT_SEARCH_PROPERTIES = ["properties.$exception_types", "properties.$exception_values", "properties.$current_url"] PROPERTY_COLUMN_NAMES = { @@ -63,6 +86,13 @@ def normalize_volume_resolution(volume_resolution: int) -> int: } +def build_event_selects(includes: list[str]) -> list[str]: + property_selects = list( + dict.fromkeys(select for include in includes for select in EVENT_CONTEXT_PROPERTY_SELECTS.get(include, [])) + ) + return ["uuid", "timestamp", "distinct_id", *property_selects] + + def compact_dict(record: dict[str, object]) -> dict[str, object]: """Remove empty response fields while intentionally preserving 0 and false values.""" return { @@ -178,8 +208,8 @@ def parse_jsonish(value: object) -> object: return parsed -def truncate_text(value: object, verbosity: str) -> object: - if verbosity == "raw" or not isinstance(value, str) or len(value) <= MAX_NORMALIZED_TEXT_CHARS: +def truncate_text(value: object) -> object: + if not isinstance(value, str) or len(value) <= MAX_NORMALIZED_TEXT_CHARS: return value suffix = f"… [truncated from {len(value)} chars]" return f"{value[: MAX_NORMALIZED_TEXT_CHARS - len(suffix)]}{suffix}" @@ -193,13 +223,17 @@ def strip_non_raw_fields(record: dict[str, object]) -> dict[str, object]: return {key: value for key, value in record.items() if key not in {"junk_drawer", "raw_id"}} -def normalize_frame(frame: object, verbosity: str, only_app_frames: bool) -> dict[str, object] | None: +def normalize_frame( + frame: object, only_app_frames: bool, include_code_variables: bool = True +) -> dict[str, object] | None: frame_record = as_record(parse_jsonish(frame)) if frame_record is None: return None if only_app_frames and frame_record.get("in_app") is not True: return None - base_frame = frame_record if verbosity == "raw" else strip_non_raw_fields(frame_record) + base_frame = strip_non_raw_fields(frame_record) + if not include_code_variables: + base_frame = {key: value for key, value in base_frame.items() if key != "code_variables"} normalized = { **base_frame, "mangled_name": frame_record.get("mangled_name") or frame_record.get("function") or frame_record.get("name"), @@ -210,7 +244,9 @@ def normalize_frame(frame: object, verbosity: str, only_app_frames: bool) -> dic return compact_dict(normalized) -def normalize_stacktrace(stacktrace: object, verbosity: str, only_app_frames: bool) -> dict[str, object] | None: +def normalize_stacktrace( + stacktrace: object, only_app_frames: bool, include_code_variables: bool = True +) -> dict[str, object] | None: stacktrace_record = as_record(parse_jsonish(stacktrace)) if stacktrace_record is None: return None @@ -219,16 +255,18 @@ def normalize_stacktrace(stacktrace: object, verbosity: str, only_app_frames: bo [ normalized for frame in cast(list[object], raw_frames) - if (normalized := normalize_frame(frame, verbosity, only_app_frames)) is not None + if (normalized := normalize_frame(frame, only_app_frames, include_code_variables)) is not None ] if isinstance(raw_frames, list) else None ) - base_stacktrace = stacktrace_record if verbosity == "raw" else strip_non_raw_fields(stacktrace_record) + base_stacktrace = strip_non_raw_fields(stacktrace_record) return compact_dict({**base_stacktrace, "frames": frames}) -def normalize_exception(exception: object, verbosity: str, only_app_frames: bool) -> dict[str, object] | None: +def normalize_exception( + exception: object, include_stacktrace: bool, only_app_frames: bool, include_code_variables: bool = True +) -> dict[str, object] | None: exception_record = as_record(parse_jsonish(exception)) if exception_record is None: return None @@ -238,22 +276,21 @@ def normalize_exception(exception: object, verbosity: str, only_app_frames: bool "value": truncate_text( exception_record.get("value") or exception_record.get("message") - or exception_record.get("exception_message"), - verbosity, + or exception_record.get("exception_message") ), "module": exception_record.get("module"), "mechanism": exception_record.get("mechanism"), } ) - if verbosity == "summary": + if not include_stacktrace: return summary - stacktrace = normalize_stacktrace(exception_record.get("stacktrace"), verbosity, only_app_frames) - if verbosity == "raw": - return compact_dict({**exception_record, "stacktrace": stacktrace}) + stacktrace = normalize_stacktrace(exception_record.get("stacktrace"), only_app_frames, include_code_variables) return compact_dict({**summary, "stacktrace": stacktrace}) -def normalize_exception_list(value: object, verbosity: str, only_app_frames: bool) -> object: +def normalize_exception_list( + value: object, include_stacktrace: bool, only_app_frames: bool, include_code_variables: bool = True +) -> object: parsed = parse_jsonish(value) record = as_record(parsed) exceptions = parsed if isinstance(parsed, list) else record.get("values") if record else None @@ -262,26 +299,31 @@ def normalize_exception_list(value: object, verbosity: str, only_app_frames: boo return [ normalized for exception in exceptions - if (normalized := normalize_exception(exception, verbosity, only_app_frames)) is not None + if (normalized := normalize_exception(exception, include_stacktrace, only_app_frames, include_code_variables)) + is not None ] -def normalize_string_array(value: object, verbosity: str = "summary", truncate_items: bool = False) -> object: +def normalize_string_array(value: object, truncate_items: bool = False) -> object: parsed = parse_jsonish(value) if not isinstance(parsed, list): return parsed - return [truncate_text(item, verbosity) for item in parsed] if truncate_items else parsed + return [truncate_text(item) for item in parsed] if truncate_items else parsed -def normalize_error_property(name: str, value: object, verbosity: str, only_app_frames: bool) -> object: +def normalize_error_property( + name: str, value: object, include_stacktrace: bool, only_app_frames: bool, include_code_variables: bool = True +) -> object: if name == "$exception_list": - return normalize_exception_list(value, verbosity, only_app_frames) - if name == "$exception_releases": + return normalize_exception_list(value, include_stacktrace, only_app_frames, include_code_variables) + if name in {"$exception_releases", "$cymbal_errors"}: return parse_jsonish(value) + if name == "$exception_handled" and isinstance(value, str) and value.lower() in {"true", "false"}: + return value.lower() == "true" if name == "$exception_types": return normalize_string_array(value) if name == "$exception_values": - return normalize_string_array(value, verbosity, True) + return normalize_string_array(value, True) return value @@ -291,7 +333,13 @@ def property_name(select: str) -> str | None: return select if select in PROPERTY_COLUMN_NAMES else None -def map_event_row(row: object, columns: list[str], verbosity: str, only_app_frames: bool) -> dict[str, object]: +def map_event_row( + row: object, + columns: list[str], + include_stacktrace: bool, + only_app_frames: bool, + include_code_variables: bool = True, +) -> dict[str, object]: if isinstance(row, list): values = row else: @@ -305,7 +353,9 @@ def map_event_row(row: object, columns: list[str], verbosity: str, only_app_fram continue prop = property_name(column) if prop: - properties[prop] = normalize_error_property(prop, value, verbosity, only_app_frames) + properties[prop] = normalize_error_property( + prop, value, include_stacktrace, only_app_frames, include_code_variables + ) else: event[column] = value return event @@ -318,7 +368,7 @@ def map_context_event_properties(data: dict[str, object]) -> dict[str, object]: return {} raw_columns = data.get("columns") columns = [str(column) for column in raw_columns] if isinstance(raw_columns, list) else CONTEXT_EVENT_SELECTS - return cast(dict[str, object], map_event_row(row, columns, "stack", True)["properties"]) + return cast(dict[str, object], map_event_row(row, columns, True, True)["properties"]) def get_frames(exception_list: object) -> list[dict[str, object]]: diff --git a/products/error_tracking/backend/presentation/views/query.py b/products/error_tracking/backend/presentation/views/query.py index ac0685de192d..39a67810c783 100644 --- a/products/error_tracking/backend/presentation/views/query.py +++ b/products/error_tracking/backend/presentation/views/query.py @@ -5,10 +5,13 @@ import structlog from drf_spectacular.utils import OpenApiResponse from rest_framework import status, viewsets +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from posthog.schema import DateRange, ErrorTrackingIssueAssignee, ErrorTrackingQuery, EventsQuery +from posthog.hogql.errors import ResolutionError + from posthog.api.mixins import ValidatedRequest, validated_request from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.utils import action @@ -21,10 +24,11 @@ ) from products.error_tracking.backend.facade.query_utils import ( CONTEXT_EVENT_SELECTS, - EVENT_SELECTS, + DEFAULT_EVENT_CONTEXT_INCLUDES, ISSUE_FIELDS, LIST_ISSUE_FIELDS, build_date_range, + build_event_selects, build_fingerprint_event_where, build_fingerprint_where, build_impact, @@ -181,9 +185,14 @@ def issue(self, request: ValidatedRequest, **kwargs: object) -> Response: tags={"productKey": "error_tracking"}, ) with tags_context(product=Product.ERROR_TRACKING, feature=Feature.QUERY): - event_data = ( - EventsQueryRunner(team=self.team, query=context_event_query).calculate().model_dump(mode="json") - ) + try: + event_data = ( + EventsQueryRunner(team=self.team, query=context_event_query, user=request.user) + .calculate() + .model_dump(mode="json") + ) + except ResolutionError as error: + raise ValidationError(str(error)) from error if event_data.get("error"): logger.warning( "error_tracking_issue_context_query_failed", @@ -230,13 +239,20 @@ def issue_events(self, request: ValidatedRequest, **kwargs: object) -> Response: if not facade_api.issue_exists_by_id(self.team.id, issue_id): return Response(status=status.HTTP_404_NOT_FOUND) date_range = build_date_range(params.get("dateRange")) + requested_includes = params.get("include") + includes = ( + cast(list[str], requested_includes) + if isinstance(requested_includes, list) + else DEFAULT_EVENT_CONTEXT_INCLUDES + ) + event_selects = build_event_selects(includes) fingerprints = facade_api.resolve_fingerprints(self.team.pk, [issue_id]) if not fingerprints: return Response({"results": [], "hasMore": False, "limit": limit, "offset": offset}) query = EventsQuery( kind="EventsQuery", event="$exception", - select=EVENT_SELECTS, + select=event_selects, where=build_fingerprint_event_where(fingerprints, cast(str | None, params.get("searchQuery"))), properties=cast(list[dict[str, object]], params.get("filterGroup", [])), filterTestAccounts=cast(bool, params.get("filterTestAccounts", True)), @@ -248,14 +264,25 @@ def issue_events(self, request: ValidatedRequest, **kwargs: object) -> Response: tags={"productKey": "error_tracking"}, ) with tags_context(product=Product.ERROR_TRACKING, feature=Feature.QUERY): - data = EventsQueryRunner(team=self.team, query=query).calculate().model_dump(mode="json") + try: + data = ( + EventsQueryRunner(team=self.team, query=query, user=request.user) + .calculate() + .model_dump(mode="json") + ) + except ResolutionError as error: + raise ValidationError(str(error)) from error raw_columns = data.get("columns") - columns = [str(column) for column in raw_columns] if isinstance(raw_columns, list) else EVENT_SELECTS + columns = [str(column) for column in raw_columns] if isinstance(raw_columns, list) else event_selects raw_results_value = data.get("results") raw_results: list[object] = raw_results_value if isinstance(raw_results_value, list) else [] - verbosity = cast(str, params.get("verbosity", "summary")) + include_stacktrace = "stacktrace" in includes or "code_variables" in includes only_app_frames = cast(bool, params.get("onlyAppFrames", True)) - results = [map_event_row(row, columns, verbosity, only_app_frames) for row in raw_results[:limit]] + include_code_variables = "code_variables" in includes + results = [ + map_event_row(row, columns, include_stacktrace, only_app_frames, include_code_variables) + for row in raw_results[:limit] + ] has_more, next_offset = get_page_info(data, limit, offset) payload: dict[str, object] = {"results": results, "hasMore": has_more, "limit": limit, "offset": offset} if next_offset is not None: diff --git a/products/error_tracking/backend/presentation/views/query_serializers.py b/products/error_tracking/backend/presentation/views/query_serializers.py index 97cb350b4b8b..d364bd25ebfb 100644 --- a/products/error_tracking/backend/presentation/views/query_serializers.py +++ b/products/error_tracking/backend/presentation/views/query_serializers.py @@ -199,11 +199,25 @@ class ErrorTrackingIssueEventsQueryRequestSerializer(serializers.Serializer): ) limit = serializers.IntegerField(required=False, min_value=1, max_value=20, default=1, help_text="Page size.") offset = serializers.IntegerField(required=False, min_value=0, default=0, help_text="Pagination offset.") - verbosity = serializers.ChoiceField( - choices=["summary", "stack", "raw"], + include = serializers.ListField( + child=serializers.ChoiceField( + choices=[ + "exception", + "stacktrace", + "code_variables", + "environment", + "release", + "navigation", + "correlation", + "diagnostics", + ] + ), required=False, - default="summary", - help_text="Controls exception detail size: summary, stack, or raw. Defaults to summary.", + help_text=( + "Context groups to return. Defaults to exception, environment, navigation, and correlation. " + "Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for " + "release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace." + ), ) onlyAppFrames = serializers.BooleanField( required=False, diff --git a/products/error_tracking/backend/tests/api/test_query_api.py b/products/error_tracking/backend/tests/api/test_query_api.py index c9a8fef5df43..ca469fbfb595 100644 --- a/products/error_tracking/backend/tests/api/test_query_api.py +++ b/products/error_tracking/backend/tests/api/test_query_api.py @@ -11,7 +11,11 @@ from dateutil.relativedelta import relativedelta from posthog.clickhouse.query_tagging import Feature, Product, get_query_tags +from posthog.constants import AvailableFeature +from posthog.models import PropertyDefinition +from products.access_control.backend.models.property_access_control import PropertyAccessControl +from products.access_control.backend.property_access_control import PropertyAccessLevel from products.error_tracking.backend.facade.query_utils import ( build_fingerprint_event_where, build_issue_filters, @@ -554,6 +558,33 @@ def test_issue_events_without_fingerprints_returns_empty(self) -> None: assert response.status_code == 200 assert response.json() == {"results": [], "hasMore": False, "limit": 1, "offset": 0} + def test_issue_events_honors_user_property_access(self) -> None: + self.organization.available_product_features = [ + {"name": AvailableFeature.PROPERTY_ACCESS_CONTROL, "key": AvailableFeature.PROPERTY_ACCESS_CONTROL} + ] + self.organization.save() + property_definition = PropertyDefinition.objects.create( + team=self.team, + name="$referrer", + type=PropertyDefinition.Type.EVENT, + ) + PropertyAccessControl.objects.create( + team=self.team, + property_definition=property_definition, + access_level=PropertyAccessLevel.NONE.value, + organization_member=self.organization_membership, + ) + self.create_issue() + + response = self.client.post( + f"/api/environments/{self.team.id}/error_tracking/query/issue_events", + data={"issueId": self.issue_id, "include": ["navigation"]}, + format="json", + ) + + assert response.status_code == 400 + assert "Access to property '$referrer' is restricted" in str(response.json()) + @freeze_time("2026-04-24T12:00:00Z") def test_issue_events_returns_plural_exception_arrays_and_truncates_summary_text(self) -> None: long_text = "x" * 1200 @@ -572,23 +603,84 @@ def test_issue_events_returns_plural_exception_arrays_and_truncates_summary_text data={"issueId": self.issue_id, "dateRange": {"date_from": "-1d", "date_to": "2026-04-25T00:00:00Z"}}, format="json", ) - raw_response = self.client.post( + assert summary_response.status_code == 200 + summary_event = summary_response.json()["results"][0] + assert summary_event["properties"]["$exception_types"] == ["TypeError"] + assert "[truncated from 1200 chars]" in summary_event["properties"]["$exception_values"][0] + assert "[truncated from 1200 chars]" in summary_event["properties"]["$exception_list"][0]["value"] + assert summary_event["properties"]["$session_id"] == "session-id-1" + + @freeze_time("2026-04-24T12:00:00Z") + def test_issue_events_returns_only_requested_context_groups(self) -> None: + self.create_issue() + self.create_exception_event( + properties={ + "$lib": "posthog-js", + "$current_url": "https://example.test/checkout", + "$exception_level": "error", + "$exception_handled": False, + "$exception_releases": {"release-id": {"version": "2026.04.24"}}, + "$cymbal_errors": ["source map unavailable"], + "$trace_id": "00000000000000000000000000000123", + "$span_id": "0000000000000456", + "$ai_trace_id": "ai-trace-id", + "$ai_span_id": "ai-span-id", + "$exception_list": [ + { + "type": "TypeError", + "value": "Cannot read properties of undefined", + "stacktrace": { + "frames": [ + { + "mangled_name": "submitOrder", + "source": "src/checkout.ts", + "line": 42, + "in_app": True, + "code_variables": {"order": {"customer": None}}, + } + ] + }, + } + ], + } + ) + flush_persons_and_events() + + stack_response = self.client.post( f"/api/environments/{self.team.id}/error_tracking/query/issue_events", data={ "issueId": self.issue_id, "dateRange": {"date_from": "-1d", "date_to": "2026-04-25T00:00:00Z"}, - "verbosity": "raw", + "include": ["stacktrace"], + }, + format="json", + ) + variables_response = self.client.post( + f"/api/environments/{self.team.id}/error_tracking/query/issue_events", + data={ + "issueId": self.issue_id, + "dateRange": {"date_from": "-1d", "date_to": "2026-04-25T00:00:00Z"}, + "include": ["exception", "code_variables", "release", "correlation", "diagnostics"], }, format="json", ) - assert summary_response.status_code == 200 - assert raw_response.status_code == 200 - summary_event = summary_response.json()["results"][0] - raw_event = raw_response.json()["results"][0] - assert summary_event["properties"]["$exception_types"] == ["TypeError"] - assert "[truncated from 1200 chars]" in summary_event["properties"]["$exception_values"][0] - assert "[truncated from 1200 chars]" in summary_event["properties"]["$exception_list"][0]["value"] - assert raw_event["properties"]["$exception_values"][0] == long_text - assert raw_event["properties"]["$exception_list"][0]["value"] == long_text - assert summary_event["properties"]["$session_id"] == "session-id-1" + assert stack_response.status_code == 200 + assert variables_response.status_code == 200 + stack_properties = stack_response.json()["results"][0]["properties"] + variables_properties = variables_response.json()["results"][0]["properties"] + stack_frame = stack_properties["$exception_list"][0]["stacktrace"]["frames"][0] + variables_frame = variables_properties["$exception_list"][0]["stacktrace"]["frames"][0] + assert "code_variables" not in stack_frame + assert variables_frame["code_variables"] == {"order": {"customer": None}} + assert variables_properties["$exception_level"] == "error" + assert variables_properties["$exception_handled"] is False + assert variables_properties["$exception_releases"] == {"release-id": {"version": "2026.04.24"}} + assert variables_properties["$cymbal_errors"] == ["source map unavailable"] + assert variables_properties["$trace_id"] == "00000000000000000000000000000123" + assert variables_properties["$span_id"] == "0000000000000456" + assert variables_properties["$ai_trace_id"] == "ai-trace-id" + assert variables_properties["$ai_span_id"] == "ai-span-id" + assert "$exception_issue_id" not in variables_properties + assert "$lib" not in variables_properties + assert "$current_url" not in variables_properties diff --git a/products/error_tracking/frontend/generated/api.schemas.ts b/products/error_tracking/frontend/generated/api.schemas.ts index 471ca5aec717..fe2ff85a073c 100644 --- a/products/error_tracking/frontend/generated/api.schemas.ts +++ b/products/error_tracking/frontend/generated/api.schemas.ts @@ -1029,16 +1029,26 @@ export const OrderDirectionEnumApi = { } as const /** - * * `summary` - summary - * * `stack` - stack - * * `raw` - raw + * * `exception` - exception + * * `stacktrace` - stacktrace + * * `code_variables` - code_variables + * * `environment` - environment + * * `release` - release + * * `navigation` - navigation + * * `correlation` - correlation + * * `diagnostics` - diagnostics */ -export type VerbosityEnumApi = (typeof VerbosityEnumApi)[keyof typeof VerbosityEnumApi] - -export const VerbosityEnumApi = { - Summary: 'summary', - Stack: 'stack', - Raw: 'raw', +export type IncludeEnumApi = (typeof IncludeEnumApi)[keyof typeof IncludeEnumApi] + +export const IncludeEnumApi = { + Exception: 'exception', + Stacktrace: 'stacktrace', + CodeVariables: 'code_variables', + Environment: 'environment', + Release: 'release', + Navigation: 'navigation', + Correlation: 'correlation', + Diagnostics: 'diagnostics', } as const export interface ErrorTrackingIssueEventsQueryRequestApi { @@ -1071,12 +1081,8 @@ export interface ErrorTrackingIssueEventsQueryRequestApi { * @minimum 0 */ offset?: number - /** Controls exception detail size: summary, stack, or raw. Defaults to summary. - * - * * `summary` - summary - * * `stack` - stack - * * `raw` - raw */ - verbosity?: VerbosityEnumApi + /** Context groups to return. Defaults to exception, environment, navigation, and correlation. Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace. */ + include?: IncludeEnumApi[] /** When true, include only stack frames marked in_app. Defaults to true. */ onlyAppFrames?: boolean } diff --git a/products/error_tracking/frontend/generated/api.zod.ts b/products/error_tracking/frontend/generated/api.zod.ts index c9b33b2789b1..d3837d0d4054 100644 --- a/products/error_tracking/frontend/generated/api.zod.ts +++ b/products/error_tracking/frontend/generated/api.zod.ts @@ -485,7 +485,6 @@ export const errorTrackingQueryIssueEventsCreateBodyLimitMax = 20 export const errorTrackingQueryIssueEventsCreateBodyOffsetDefault = 0 export const errorTrackingQueryIssueEventsCreateBodyOffsetMin = 0 -export const errorTrackingQueryIssueEventsCreateBodyVerbosityDefault = `summary` export const errorTrackingQueryIssueEventsCreateBodyOnlyAppFramesDefault = true export const ErrorTrackingQueryIssueEventsCreateBody = /* @__PURE__ */ zod.object({ @@ -617,12 +616,26 @@ export const ErrorTrackingQueryIssueEventsCreateBody = /* @__PURE__ */ zod.objec .min(errorTrackingQueryIssueEventsCreateBodyOffsetMin) .default(errorTrackingQueryIssueEventsCreateBodyOffsetDefault) .describe('Pagination offset.'), - verbosity: zod - .enum(['summary', 'stack', 'raw']) - .describe('\* `summary` - summary\n\* `stack` - stack\n\* `raw` - raw') - .default(errorTrackingQueryIssueEventsCreateBodyVerbosityDefault) + include: zod + .array( + zod + .enum([ + 'exception', + 'stacktrace', + 'code_variables', + 'environment', + 'release', + 'navigation', + 'correlation', + 'diagnostics', + ]) + .describe( + '\* `exception` - exception\n\* `stacktrace` - stacktrace\n\* `code_variables` - code_variables\n\* `environment` - environment\n\* `release` - release\n\* `navigation` - navigation\n\* `correlation` - correlation\n\* `diagnostics` - diagnostics' + ) + ) + .optional() .describe( - 'Controls exception detail size: summary, stack, or raw. Defaults to summary.\n\n\* `summary` - summary\n\* `stack` - stack\n\* `raw` - raw' + 'Context groups to return. Defaults to exception, environment, navigation, and correlation. Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace.' ), onlyAppFrames: zod .boolean() diff --git a/products/error_tracking/mcp/prompts/query-error-tracking-issue-events.md b/products/error_tracking/mcp/prompts/query-error-tracking-issue-events.md index d1d018af85cd..090c2d202b3b 100644 --- a/products/error_tracking/mcp/prompts/query-error-tracking-issue-events.md +++ b/products/error_tracking/mcp/prompts/query-error-tracking-issue-events.md @@ -1,8 +1,8 @@ Fetch sampled `$exception` events for one Error tracking issue. -Use this when the user asks for concrete examples, stack traces, affected URLs, browser/OS/library context, or Session replay links for a specific issue. +Use this when the user asks for concrete examples, stack traces, code variables, affected URLs, browser/OS/library context, release details, diagnostics, or Session replay links for a specific issue. -Returns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_issue_id`, `$session_id`, `$lib`, browser/OS fields, and `$current_url`. +Returns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_level`, `$exception_handled`, `$session_id`, OpenTelemetry and AI trace/span IDs, `$lib`, browser/OS fields, and `$current_url`. # Parameters @@ -10,7 +10,7 @@ Returns sampled events with plural exception fields (`$exception_types`, `$excep - `dateRange`: time range for sampled events. Defaults to last 7 days. - `searchQuery`: search exception types, values, and current URL. - `filterGroup`: advanced flat AND property filters applied to sampled events. -- `verbosity`: `summary` (default), `stack`, or `raw`. Use `raw` only when exact untruncated exception payloads are needed. +- `include`: context groups to return. Defaults to compact exception, environment, navigation, and correlation context. Add `stacktrace`, `code_variables`, `release`, or `diagnostics` only when needed. `code_variables` implies stack frames and may contain SDK-masked sensitive values. - `onlyAppFrames`: defaults to true to reduce vendor-frame noise. - `limit`: defaults to 1 and maxes at 20. Keep low unless the user asks for multiple examples. diff --git a/products/error_tracking/mcp/tools.yaml b/products/error_tracking/mcp/tools.yaml index 14a6aed4c74e..9fe4daaea836 100644 --- a/products/error_tracking/mcp/tools.yaml +++ b/products/error_tracking/mcp/tools.yaml @@ -583,7 +583,7 @@ tools: enrich_url: '{params.issueId}' title: Query error tracking issue events description_file: ./prompts/query-error-tracking-issue-events.md - system_prompt_hint: Error event samples, stack traces, and session IDs (error-tracking category) + system_prompt_hint: Error event samples, stack traces, code variables, and session IDs (error-tracking category) ui_app: error-details query-error-tracking-issues-list: operation: error_tracking_query_issues_list_create diff --git a/products/error_tracking/skills/grouping-noisy-errors/SKILL.md b/products/error_tracking/skills/grouping-noisy-errors/SKILL.md index 527089e589d1..0bba8aeff4ec 100644 --- a/products/error_tracking/skills/grouping-noisy-errors/SKILL.md +++ b/products/error_tracking/skills/grouping-noisy-errors/SKILL.md @@ -86,7 +86,7 @@ posthog:query-error-tracking-issue-events { "issueId": "", "limit": 1, - "verbosity": "stack" + "include": ["exception", "stacktrace", "environment"] } ``` diff --git a/products/error_tracking/skills/investigating-error-issue/SKILL.md b/products/error_tracking/skills/investigating-error-issue/SKILL.md index b995df98cdb8..276a817fb4ec 100644 --- a/products/error_tracking/skills/investigating-error-issue/SKILL.md +++ b/products/error_tracking/skills/investigating-error-issue/SKILL.md @@ -62,12 +62,11 @@ posthog:query-error-tracking-issue-events { "issueId": "", "limit": 1, - "verbosity": "stack" + "include": ["exception", "stacktrace", "environment", "navigation", "correlation"] } ``` -Use `verbosity: "raw"` only if the truncated stack hides the answer. The tool -defaults to `onlyAppFrames: true`, which strips vendor frames; flip to `false` +The tool defaults to `onlyAppFrames: true`, which strips vendor frames; flip to `false` when the bug appears to live in a third-party library — or when the response comes back with `stacktrace.type: "resolved"` but no frames at all (common for minified bundles where every frame looks vendor-y to the resolver, e.g. React diff --git a/products/error_tracking/skills/suppressing-noisy-errors/SKILL.md b/products/error_tracking/skills/suppressing-noisy-errors/SKILL.md index d22b7a82870e..c3fe3d6cdc9f 100644 --- a/products/error_tracking/skills/suppressing-noisy-errors/SKILL.md +++ b/products/error_tracking/skills/suppressing-noisy-errors/SKILL.md @@ -92,7 +92,7 @@ posthog:query-error-tracking-issue-events { "issueId": "", "limit": 10, - "verbosity": "stack" + "include": ["exception", "stacktrace", "environment", "navigation"] } ``` @@ -225,7 +225,7 @@ posthog:query-error-tracking-issue-events { "issueId": "", "limit": 3, - "verbosity": "stack", + "include": ["exception", "stacktrace", "environment", "navigation"], "onlyAppFrames": false } ``` diff --git a/products/error_tracking/skills/triaging-error-issues/SKILL.md b/products/error_tracking/skills/triaging-error-issues/SKILL.md index 0035ce3b7de2..7a6cf7cc1b68 100644 --- a/products/error_tracking/skills/triaging-error-issues/SKILL.md +++ b/products/error_tracking/skills/triaging-error-issues/SKILL.md @@ -116,7 +116,7 @@ posthog:query-error-tracking-issue-events { "issueId": "", "limit": 1, - "verbosity": "stack" + "include": ["exception", "stacktrace", "environment", "navigation", "correlation"] } ``` diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json index 1d102727d7fa..39beb2ea87fb 100644 --- a/services/mcp/schema/generated-tool-definitions.json +++ b/services/mcp/schema/generated-tool-definitions.json @@ -6345,7 +6345,7 @@ "system_prompt_hint": "Error issue details and impact (error-tracking category)" }, "query-error-tracking-issue-events": { - "description": "Fetch sampled `$exception` events for one Error tracking issue.\n\nUse this when the user asks for concrete examples, stack traces, affected URLs, browser/OS/library context, or Session replay links for a specific issue.\n\nReturns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_issue_id`, `$session_id`, `$lib`, browser/OS fields, and `$current_url`.\n\n# Parameters\n\n- `issueId`: required Error tracking issue UUID.\n- `dateRange`: time range for sampled events. Defaults to last 7 days.\n- `searchQuery`: search exception types, values, and current URL.\n- `filterGroup`: advanced flat AND property filters applied to sampled events.\n- `verbosity`: `summary` (default), `stack`, or `raw`. Use `raw` only when exact untruncated exception payloads are needed.\n- `onlyAppFrames`: defaults to true to reduce vendor-frame noise.\n- `limit`: defaults to 1 and maxes at 20. Keep low unless the user asks for multiple examples.\n\n# Session recordings\n\nWhen `$session_id` is present and the user asks what happened before the error, call `query-session-recordings-list` with `session_ids` to fetch matching recordings. Use multiple `$session_id` values in one call when available.", + "description": "Fetch sampled `$exception` events for one Error tracking issue.\n\nUse this when the user asks for concrete examples, stack traces, code variables, affected URLs, browser/OS/library context, release details, diagnostics, or Session replay links for a specific issue.\n\nReturns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_level`, `$exception_handled`, `$session_id`, OpenTelemetry and AI trace/span IDs, `$lib`, browser/OS fields, and `$current_url`.\n\n# Parameters\n\n- `issueId`: required Error tracking issue UUID.\n- `dateRange`: time range for sampled events. Defaults to last 7 days.\n- `searchQuery`: search exception types, values, and current URL.\n- `filterGroup`: advanced flat AND property filters applied to sampled events.\n- `include`: context groups to return. Defaults to compact exception, environment, navigation, and correlation context. Add `stacktrace`, `code_variables`, `release`, or `diagnostics` only when needed. `code_variables` implies stack frames and may contain SDK-masked sensitive values.\n- `onlyAppFrames`: defaults to true to reduce vendor-frame noise.\n- `limit`: defaults to 1 and maxes at 20. Keep low unless the user asks for multiple examples.\n\n# Session recordings\n\nWhen `$session_id` is present and the user asks what happened before the error, call `query-session-recordings-list` with `session_ids` to fetch matching recordings. Use multiple `$session_id` values in one call when available.", "category": "Error tracking", "feature": "error_tracking", "summary": "Query error tracking issue events", @@ -6357,7 +6357,7 @@ "openWorldHint": true, "readOnlyHint": true }, - "system_prompt_hint": "Error event samples, stack traces, and session IDs (error-tracking category)" + "system_prompt_hint": "Error event samples, stack traces, code variables, and session IDs (error-tracking category)" }, "query-error-tracking-issues-list": { "description": "List and filter Error tracking issues. Returns compact issue rows with aggregate impact counts (`occurrences`, `users`, `sessions`) and optional volume buckets.\n\nUse this first when the user asks which errors are happening, which errors are most common, or wants to narrow issues by status, release, library, fingerprint, URL, user, person, or properties.\n\nDefaults are intentionally useful: active issues, last 7 days, sorted by occurrences, test accounts filtered out, and compact aggregate counts.\n\nBe minimalist. Only add filters needed to answer the user’s question. Do not add \"is set\" filters unless the user explicitly asks for them.\n\n# Common filters\n\n- `status`: `active`, `resolved`, `suppressed`, `pending_release`, `archived`, or `all`. Defaults to `active`.\n- `searchQuery`: free-text search for exception names, values, stack frames, and email text.\n- `library`: exact `$lib` match, for example `posthog-js`.\n- `release`: exact release ID, release version, or git commit ID captured in `$exception_releases`. This intentionally does not match project name, branch, or timestamp fragments.\n- `fingerprint`: exact `$exception_fingerprint` match.\n- `url`: substring match on `$current_url`.\n- `personId`: exact PostHog person UUID.\n- `user`: user/email text search.\n- `filePath`: stack-frame file/source text search.\n- `filterGroup`: advanced flat AND property filters. Prefer typed fields above when they fit.\n\nUse `dateRange` for time, not property filters. Omit `date_to` for now.\n\n# Next steps\n\n- Use `query-error-tracking-issue` with `issueId` to inspect one issue.\n- Use `query-error-tracking-issue-events` with `issueId` to fetch sampled exception events, stack traces, URLs, and `$session_id` values.\n- If the user asks what people were doing before the error, use `$session_id` values from issue events with `query-session-recordings-list` and its `session_ids` parameter.", diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json index cd6efab22ba1..a40e60926211 100644 --- a/services/mcp/schema/tool-definitions-all.json +++ b/services/mcp/schema/tool-definitions-all.json @@ -6823,7 +6823,7 @@ "system_prompt_hint": "Error issue details and impact (error-tracking category)" }, "query-error-tracking-issue-events": { - "description": "Fetch sampled `$exception` events for one Error tracking issue.\n\nUse this when the user asks for concrete examples, stack traces, affected URLs, browser/OS/library context, or Session replay links for a specific issue.\n\nReturns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_issue_id`, `$session_id`, `$lib`, browser/OS fields, and `$current_url`.\n\n# Parameters\n\n- `issueId`: required Error tracking issue UUID.\n- `dateRange`: time range for sampled events. Defaults to last 7 days.\n- `searchQuery`: search exception types, values, and current URL.\n- `filterGroup`: advanced flat AND property filters applied to sampled events.\n- `verbosity`: `summary` (default), `stack`, or `raw`. Use `raw` only when exact untruncated exception payloads are needed.\n- `onlyAppFrames`: defaults to true to reduce vendor-frame noise.\n- `limit`: defaults to 1 and maxes at 20. Keep low unless the user asks for multiple examples.\n\n# Session recordings\n\nWhen `$session_id` is present and the user asks what happened before the error, call `query-session-recordings-list` with `session_ids` to fetch matching recordings. Use multiple `$session_id` values in one call when available.", + "description": "Fetch sampled `$exception` events for one Error tracking issue.\n\nUse this when the user asks for concrete examples, stack traces, code variables, affected URLs, browser/OS/library context, release details, diagnostics, or Session replay links for a specific issue.\n\nReturns sampled events with plural exception fields (`$exception_types`, `$exception_values`), normalized `$exception_list`, `$exception_fingerprint`, `$exception_level`, `$exception_handled`, `$session_id`, OpenTelemetry and AI trace/span IDs, `$lib`, browser/OS fields, and `$current_url`.\n\n# Parameters\n\n- `issueId`: required Error tracking issue UUID.\n- `dateRange`: time range for sampled events. Defaults to last 7 days.\n- `searchQuery`: search exception types, values, and current URL.\n- `filterGroup`: advanced flat AND property filters applied to sampled events.\n- `include`: context groups to return. Defaults to compact exception, environment, navigation, and correlation context. Add `stacktrace`, `code_variables`, `release`, or `diagnostics` only when needed. `code_variables` implies stack frames and may contain SDK-masked sensitive values.\n- `onlyAppFrames`: defaults to true to reduce vendor-frame noise.\n- `limit`: defaults to 1 and maxes at 20. Keep low unless the user asks for multiple examples.\n\n# Session recordings\n\nWhen `$session_id` is present and the user asks what happened before the error, call `query-session-recordings-list` with `session_ids` to fetch matching recordings. Use multiple `$session_id` values in one call when available.", "category": "Error tracking", "feature": "error_tracking", "summary": "Query error tracking issue events", @@ -6835,7 +6835,7 @@ "openWorldHint": true, "readOnlyHint": true }, - "system_prompt_hint": "Error event samples, stack traces, and session IDs (error-tracking category)" + "system_prompt_hint": "Error event samples, stack traces, code variables, and session IDs (error-tracking category)" }, "query-error-tracking-issues-list": { "description": "List and filter Error tracking issues. Returns compact issue rows with aggregate impact counts (`occurrences`, `users`, `sessions`) and optional volume buckets.\n\nUse this first when the user asks which errors are happening, which errors are most common, or wants to narrow issues by status, release, library, fingerprint, URL, user, person, or properties.\n\nDefaults are intentionally useful: active issues, last 7 days, sorted by occurrences, test accounts filtered out, and compact aggregate counts.\n\nBe minimalist. Only add filters needed to answer the user’s question. Do not add \"is set\" filters unless the user explicitly asks for them.\n\n# Common filters\n\n- `status`: `active`, `resolved`, `suppressed`, `pending_release`, `archived`, or `all`. Defaults to `active`.\n- `searchQuery`: free-text search for exception names, values, stack frames, and email text.\n- `library`: exact `$lib` match, for example `posthog-js`.\n- `release`: exact release ID, release version, or git commit ID captured in `$exception_releases`. This intentionally does not match project name, branch, or timestamp fragments.\n- `fingerprint`: exact `$exception_fingerprint` match.\n- `url`: substring match on `$current_url`.\n- `personId`: exact PostHog person UUID.\n- `user`: user/email text search.\n- `filePath`: stack-frame file/source text search.\n- `filterGroup`: advanced flat AND property filters. Prefer typed fields above when they fit.\n\nUse `dateRange` for time, not property filters. Omit `date_to` for now.\n\n# Next steps\n\n- Use `query-error-tracking-issue` with `issueId` to inspect one issue.\n- Use `query-error-tracking-issue-events` with `issueId` to fetch sampled exception events, stack traces, URLs, and `$session_id` values.\n- If the user asks what people were doing before the error, use `$session_id` values from issue events with `query-session-recordings-list` and its `session_ids` parameter.", diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index 4043033bbd73..6f8c6eefe231 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -22025,17 +22025,27 @@ export namespace Schemas { } as const; /** - * * `summary` - summary - * * `stack` - stack - * * `raw` - raw + * * `exception` - exception + * * `stacktrace` - stacktrace + * * `code_variables` - code_variables + * * `environment` - environment + * * `release` - release + * * `navigation` - navigation + * * `correlation` - correlation + * * `diagnostics` - diagnostics */ - export type VerbosityEnum = typeof VerbosityEnum[keyof typeof VerbosityEnum]; + export type IncludeEnum = typeof IncludeEnum[keyof typeof IncludeEnum]; - export const VerbosityEnum = { - Summary: 'summary', - Stack: 'stack', - Raw: 'raw', + export const IncludeEnum = { + Exception: 'exception', + Stacktrace: 'stacktrace', + CodeVariables: 'code_variables', + Environment: 'environment', + Release: 'release', + Navigation: 'navigation', + Correlation: 'correlation', + Diagnostics: 'diagnostics', } as const; export interface ErrorTrackingIssueEventsQueryRequest { @@ -22068,12 +22078,8 @@ export namespace Schemas { * @minimum 0 */ offset?: number; - /** Controls exception detail size: summary, stack, or raw. Defaults to summary. - * - * * `summary` - summary - * * `stack` - stack - * * `raw` - raw */ - verbosity?: VerbosityEnum; + /** Context groups to return. Defaults to exception, environment, navigation, and correlation. Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace. */ + include?: IncludeEnum[]; /** When true, include only stack frames marked in_app. Defaults to true. */ onlyAppFrames?: boolean; } diff --git a/services/mcp/src/generated/error_tracking/api.ts b/services/mcp/src/generated/error_tracking/api.ts index 7fabe14506ad..5954deaad78c 100644 --- a/services/mcp/src/generated/error_tracking/api.ts +++ b/services/mcp/src/generated/error_tracking/api.ts @@ -5646,7 +5646,6 @@ export const errorTrackingQueryIssueEventsCreateBodyLimitMax = 20 export const errorTrackingQueryIssueEventsCreateBodyOffsetDefault = 0 export const errorTrackingQueryIssueEventsCreateBodyOffsetMin = 0 -export const errorTrackingQueryIssueEventsCreateBodyVerbosityDefault = `summary` export const errorTrackingQueryIssueEventsCreateBodyOnlyAppFramesDefault = true export const ErrorTrackingQueryIssueEventsCreateBody = /* @__PURE__ */ zod.object({ @@ -5778,12 +5777,26 @@ export const ErrorTrackingQueryIssueEventsCreateBody = /* @__PURE__ */ zod.objec .min(errorTrackingQueryIssueEventsCreateBodyOffsetMin) .default(errorTrackingQueryIssueEventsCreateBodyOffsetDefault) .describe('Pagination offset.'), - verbosity: zod - .enum(['summary', 'stack', 'raw']) - .describe('* `summary` - summary\n* `stack` - stack\n* `raw` - raw') - .default(errorTrackingQueryIssueEventsCreateBodyVerbosityDefault) + include: zod + .array( + zod + .enum([ + 'exception', + 'stacktrace', + 'code_variables', + 'environment', + 'release', + 'navigation', + 'correlation', + 'diagnostics', + ]) + .describe( + '* `exception` - exception\n* `stacktrace` - stacktrace\n* `code_variables` - code_variables\n* `environment` - environment\n* `release` - release\n* `navigation` - navigation\n* `correlation` - correlation\n* `diagnostics` - diagnostics' + ) + ) + .optional() .describe( - 'Controls exception detail size: summary, stack, or raw. Defaults to summary.\n\n* `summary` - summary\n* `stack` - stack\n* `raw` - raw' + 'Context groups to return. Defaults to exception, environment, navigation, and correlation. Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace.' ), onlyAppFrames: zod .boolean() diff --git a/services/mcp/src/tools/generated/error_tracking.ts b/services/mcp/src/tools/generated/error_tracking.ts index 3dbf7b665339..6366810730d5 100644 --- a/services/mcp/src/tools/generated/error_tracking.ts +++ b/services/mcp/src/tools/generated/error_tracking.ts @@ -643,8 +643,8 @@ const queryErrorTrackingIssueEvents = (): ToolBase< if (params.offset !== undefined) { body['offset'] = params.offset } - if (params.verbosity !== undefined) { - body['verbosity'] = params.verbosity + if (params.include !== undefined) { + body['include'] = params.include } if (params.onlyAppFrames !== undefined) { body['onlyAppFrames'] = params.onlyAppFrames diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/query-error-tracking-issue-events.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/query-error-tracking-issue-events.json index f3190c5b2989..d6b9c3c88249 100644 --- a/services/mcp/tests/unit/__snapshots__/tool-schemas/query-error-tracking-issue-events.json +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/query-error-tracking-issue-events.json @@ -147,6 +147,24 @@ "description": "When true, exclude internal/test account data from results. Defaults to true.", "type": "boolean" }, + "include": { + "description": "Context groups to return. Defaults to exception, environment, navigation, and correlation. Request stacktrace for frames, code_variables for captured and SDK-masked frame variables, release for release metadata, or diagnostics for ingestion errors. code_variables implies stacktrace.", + "items": { + "description": "* `exception` - exception\n* `stacktrace` - stacktrace\n* `code_variables` - code_variables\n* `environment` - environment\n* `release` - release\n* `navigation` - navigation\n* `correlation` - correlation\n* `diagnostics` - diagnostics", + "enum": [ + "exception", + "stacktrace", + "code_variables", + "environment", + "release", + "navigation", + "correlation", + "diagnostics" + ], + "type": "string" + }, + "type": "array" + }, "issueId": { "description": "Error tracking issue ID.", "type": "string" @@ -179,12 +197,6 @@ "description": "Search exception types, exception values, and current URL among sampled events.", "maxLength": 500, "type": "string" - }, - "verbosity": { - "default": "summary", - "description": "Controls exception detail size: summary, stack, or raw. Defaults to summary.\n\n* `summary` - summary\n* `stack` - stack\n* `raw` - raw", - "enum": ["summary", "stack", "raw"], - "type": "string" } }, "required": ["issueId"],