Coverage for schemas/schemas.py: 73%
805 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
1from typing import Annotated, List, Literal, Optional, Union
3from pydantic import (
4 AfterValidator,
5 AnyHttpUrl,
6 EmailStr,
7 Field,
8 HttpUrl,
9 SecretStr,
10 computed_field,
11 field_validator,
12 model_validator,
13)
14from pydantic.functional_validators import BeforeValidator
16from .overrides import BaseModel, Enum
17from .transformers_validators import (
18 NAME_PATTERN,
19 check_account_name,
20 check_alphanumeric,
21 check_property_name,
22 check_regex,
23 int_to_string,
24 remove_duplicate_values,
25 remove_whitespace,
26 single_to_list,
27 transform_email,
28)
31class _GRecaptcha(BaseModel):
32 g_recaptcha_response: Optional[str] = Field(
33 default=None, alias="g-recaptcha-response"
34 )
37class UserLoginSchema(_GRecaptcha):
38 email: EmailStr = Field(...)
39 password: SecretStr = Field(...)
41 _transform_email = field_validator("email", mode="before")(transform_email)
44class UserSignupSchema(UserLoginSchema):
45 fullname: str = Field(..., min_length=1)
46 organizationName: str = Field(..., min_length=1)
48 _transform_fullname = field_validator("fullname", mode="before")(remove_whitespace)
49 _transform_organizationName = field_validator("organizationName", mode="before")(
50 remove_whitespace
51 )
53 _check_account_name = field_validator("fullname", "organizationName")(
54 check_account_name
55 )
58class EditAccountSchema(BaseModel):
59 name: Optional[str] = Field(default=None)
60 tenantName: Optional[str] = Field(default=None)
61 opt_out: Optional[bool] = Field(default=None)
63 _transform_name = field_validator("name", mode="before")(remove_whitespace)
64 _transform_tenantName = field_validator("tenantName", mode="before")(
65 remove_whitespace
66 )
67 _check_account_name = field_validator("name", "tenantName")(check_account_name)
70class ForgetPasswordPayloadSchema(_GRecaptcha):
71 email: EmailStr = Field(...)
73 _transform_email = field_validator("email", mode="before")(transform_email)
76class EditUserPasswordSchema(BaseModel):
77 old_password: SecretStr = Field(...)
78 new_password: SecretStr = Field(...)
81class CreateProjectSchema(BaseModel):
82 name: str = Field(default="my first project", pattern=NAME_PATTERN)
83 platform: Literal["web", "ios"] = Field(default="web")
85 _transform_name = field_validator("name", mode="before")(remove_whitespace)
88class ProjectContext(BaseModel):
89 project_id: int = Field(..., gt=0)
90 project_key: str = Field(...)
91 name: str = Field(...)
92 platform: Literal["web", "ios"] = Field(...)
95class CurrentAPIContext(BaseModel):
96 tenant_id: int = Field(...)
97 project: Optional[ProjectContext] = Field(default=None)
100class CurrentContext(CurrentAPIContext):
101 user_id: int = Field(...)
102 email: EmailStr = Field(...)
103 role: str = Field(...)
105 _transform_email = field_validator("email", mode="before")(transform_email)
107 @computed_field
108 @property
109 def is_owner(self) -> bool:
110 return self.role == "owner"
112 @computed_field
113 @property
114 def is_admin(self) -> bool:
115 return self.role == "admin"
117 @computed_field
118 @property
119 def is_member(self) -> bool:
120 return self.role == "member"
123class AddCollaborationSchema(BaseModel):
124 name: str = Field(..., pattern=NAME_PATTERN)
125 url: HttpUrl = Field(...)
127 _transform_name = field_validator("name", mode="before")(remove_whitespace)
128 _transform_url = field_validator("url", mode="before")(remove_whitespace)
131class EditCollaborationSchema(AddCollaborationSchema):
132 name: Optional[str] = Field(default=None, pattern=NAME_PATTERN)
135class _TimedSchema(BaseModel):
136 startTimestamp: int = Field(default=None)
137 endTimestamp: int = Field(default=None)
139 @model_validator(mode="before")
140 @classmethod
141 def transform_time(cls, values):
142 if values.get("startTimestamp") is None and values.get("startDate") is not None: 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true
143 values["startTimestamp"] = values["startDate"]
144 if values.get("endTimestamp") is None and values.get("endDate") is not None: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 values["endTimestamp"] = values["endDate"]
146 return values
148 @model_validator(mode="after")
149 def __time_validator(self):
150 if self.startTimestamp is not None:
151 assert 0 <= self.startTimestamp, (
152 "startTimestamp must be greater or equal to 0"
153 )
154 if self.endTimestamp is not None:
155 assert 0 <= self.endTimestamp, "endTimestamp must be greater or equal to 0"
156 if self.startTimestamp is not None and self.endTimestamp is not None:
157 assert self.startTimestamp <= self.endTimestamp, (
158 "endTimestamp must be greater or equal to startTimestamp"
159 )
160 return self
163class NotificationsViewSchema(_TimedSchema):
164 ids: List[int] = Field(default_factory=list)
165 startTimestamp: Optional[int] = Field(default=None)
166 endTimestamp: Optional[int] = Field(default=None)
169class IssueTrackingIntegration(BaseModel):
170 token: str = Field(...)
173class IssueTrackingGithubSchema(IssueTrackingIntegration):
174 pass
177class IssueTrackingJiraSchema(IssueTrackingIntegration):
178 username: str = Field(...)
179 url: HttpUrl = Field(...)
181 @field_validator("url")
182 @classmethod
183 def transform_url(cls, v: HttpUrl):
184 return HttpUrl.build(scheme=v.scheme.lower(), host=v.host.lower())
187class WebhookSchema(BaseModel):
188 webhook_id: Optional[int] = Field(default=None)
189 processed_endpoint: AnyHttpUrl = Field(..., alias="endpoint")
190 endpoint: Optional[str] = Field(default=None, doc_hidden=True)
191 auth_header: Optional[str] = Field(default=None)
192 name: str = Field(default="", max_length=100, pattern=NAME_PATTERN)
194 _transform_name = field_validator("name", mode="before")(remove_whitespace)
197class CreateMemberSchema(BaseModel):
198 user_id: Optional[int] = Field(default=None)
199 name: str = Field(...)
200 email: EmailStr = Field(...)
201 admin: Optional[bool] = Field(default=False)
203 _transform_email = field_validator("email", mode="before")(transform_email)
204 _transform_name = field_validator("name", mode="before")(remove_whitespace)
207class EditMemberSchema(BaseModel):
208 name: str = Field(...)
209 email: EmailStr = Field(...)
210 admin: bool = Field(default=False)
212 _transform_email = field_validator("email", mode="before")(transform_email)
213 _transform_name = field_validator("name", mode="before")(remove_whitespace)
214 _check_alphanumeric = field_validator("name")(check_alphanumeric)
217class EditPasswordByInvitationSchema(BaseModel):
218 invitation: str = Field(...)
219 passphrase: str = Field(..., alias="pass")
220 password: SecretStr = Field(...)
223class AssignmentSchema(BaseModel):
224 assignee: str = Field(...)
225 description: str = Field(...)
226 title: str = Field(...)
227 issue_type: str = Field(...)
229 _transform_title = field_validator("title", mode="before")(remove_whitespace)
232class CommentAssignmentSchema(BaseModel):
233 message: str = Field(...)
236class IntegrationNotificationSchema(BaseModel):
237 comment: Optional[str] = Field(default=None)
240class GdprSchema(BaseModel):
241 maskEmails: bool = Field(...)
242 sampleRate: int = Field(...)
243 maskNumbers: bool = Field(...)
244 defaultInputMode: str = Field(...)
247class SampleRateSchema(BaseModel):
248 rate: int = Field(..., ge=0, le=100)
249 capture_all: bool = Field(default=False)
252class WeeklyReportConfigSchema(BaseModel):
253 weekly_report: bool = Field(default=True)
256class IntegrationBase(BaseModel):
257 pass
260class IntegrationSentrySchema(IntegrationBase):
261 project_slug: str = Field(...)
262 organization_slug: str = Field(...)
263 token: str = Field(...)
266class IntegrationDatadogSchema(IntegrationBase):
267 api_key: str = Field(...)
268 application_key: str = Field(...)
271class IntegartionStackdriverSchema(IntegrationBase):
272 service_account_credentials: str = Field(...)
273 log_name: str = Field(...)
276class IntegrationNewrelicSchema(IntegrationBase):
277 application_id: str = Field(...)
278 x_query_key: str = Field(...)
279 region: bool = Field(default=False)
282class IntegrationRollbarSchema(IntegrationBase):
283 access_token: str = Field(...)
286class IntegrationBugsnagBasicSchema(IntegrationBase):
287 authorization_token: str = Field(...)
290class IntegrationBugsnagSchema(IntegrationBugsnagBasicSchema):
291 bugsnag_project_id: str = Field(...)
294class IntegrationCloudwatchBasicSchema(IntegrationBase):
295 aws_access_key_id: str = Field(...)
296 aws_secret_access_key: str = Field(...)
297 region: str = Field(...)
300class IntegrationCloudwatchSchema(IntegrationCloudwatchBasicSchema):
301 log_group_name: str = Field(...)
304class IntegrationElasticsearchTestSchema(IntegrationBase):
305 host: str = Field(...)
306 port: int = Field(...)
307 api_key_id: Optional[str] = Field(default=None)
308 api_key: str = Field(...)
311class IntegrationElasticsearchSchema(IntegrationElasticsearchTestSchema):
312 indexes: str = Field(...)
315class IntegrationSumologicSchema(IntegrationBase):
316 access_id: str = Field(...)
317 access_key: str = Field(...)
318 region: str = Field(...)
321class MetadataSchema(BaseModel):
322 index: Optional[int] = Field(default=None)
323 key: str = Field(...)
325 _transform_key = field_validator("key", mode="before")(remove_whitespace)
328class _AlertMessageSchema(BaseModel):
329 type: str = Field(...)
330 value: str = Field(...)
332 _transform_value = field_validator("value", mode="before")(int_to_string)
335class AlertDetectionType(str, Enum):
336 PERCENT = "percent"
337 CHANGE = "change"
340class _AlertOptionSchema(BaseModel):
341 message: List[_AlertMessageSchema] = Field([])
342 currentPeriod: Literal[15, 30, 60, 120, 240, 1440] = Field(...)
343 previousPeriod: Literal[15, 30, 60, 120, 240, 1440] = Field(default=15)
344 lastNotification: Optional[int] = Field(default=None)
345 renotifyInterval: Optional[int] = Field(default=720)
348class AlertColumn(str, Enum):
349 PERFORMANCE__DOM_CONTENT_LOADED__AVERAGE = "performance.dom_content_loaded.average"
350 PERFORMANCE__FIRST_MEANINGFUL_PAINT__AVERAGE = (
351 "performance.first_meaningful_paint.average"
352 )
353 PERFORMANCE__PAGE_LOAD_TIME__AVERAGE = "performance.page_load_time.average"
354 PERFORMANCE__DOM_BUILD_TIME__AVERAGE = "performance.dom_build_time.average"
355 PERFORMANCE__SPEED_INDEX__AVERAGE = "performance.speed_index.average"
356 PERFORMANCE__PAGE_RESPONSE_TIME__AVERAGE = "performance.page_response_time.average"
357 PERFORMANCE__TTFB__AVERAGE = "performance.ttfb.average"
358 PERFORMANCE__TIME_TO_RENDER__AVERAGE = "performance.time_to_render.average"
359 PERFORMANCE__CRASHES__COUNT = "performance.crashes.count"
360 ERRORS__JAVASCRIPT__COUNT = "errors.javascript.count"
361 ERRORS__BACKEND__COUNT = "errors.backend.count"
362 CUSTOM = "CUSTOM"
365class MathOperator(str, Enum):
366 EQUAL = "="
367 LESS = "<"
368 GREATER = ">"
369 LESS_EQ = "<="
370 GREATER_EQ = ">="
373class _AlertQuerySchema(BaseModel):
374 left: Union[AlertColumn, int] = Field(...)
375 right: float = Field(...)
376 operator: MathOperator = Field(...)
379class AlertDetectionMethod(str, Enum):
380 THRESHOLD = "threshold"
381 CHANGE = "change"
384class AlertSchema(BaseModel):
385 name: str = Field(..., pattern=NAME_PATTERN)
386 detection_method: AlertDetectionMethod = Field(...)
387 change: Optional[AlertDetectionType] = Field(default=AlertDetectionType.CHANGE)
388 description: Optional[str] = Field(default=None)
389 options: _AlertOptionSchema = Field(...)
390 query: _AlertQuerySchema = Field(...)
391 series_id: Optional[int] = Field(default=None, doc_hidden=True)
393 @model_validator(mode="after")
394 def transform_alert(self):
395 self.series_id = None
396 if isinstance(self.query.left, int):
397 self.series_id = self.query.left
398 self.query.left = AlertColumn.CUSTOM
400 return self
403class SourcemapUploadPayloadSchema(BaseModel):
404 urls: List[str] = Field(..., alias="URL")
407class ErrorSource(str, Enum):
408 JS_EXCEPTION = "js_exception"
409 BUGSNAG = "bugsnag"
410 CLOUDWATCH = "cloudwatch"
411 DATADOG = "datadog"
412 NEWRELIC = "newrelic"
413 ROLLBAR = "rollbar"
414 SENTRY = "sentry"
415 STACKDRIVER = "stackdriver"
416 SUMOLOGIC = "sumologic"
419class EventType(str, Enum):
420 CLICK = "click"
421 INPUT = "input"
422 LOCATION = "location"
423 CUSTOM = "custom"
424 REQUEST = "request"
425 REQUEST_DETAILS = "fetch"
426 GRAPHQL = "graphql"
427 STATE_ACTION = "stateAction"
428 ERROR = "error"
429 TAG = "tag"
430 CLICK_MOBILE = "clickMobile"
431 INPUT_MOBILE = "inputMobile"
432 VIEW_MOBILE = "viewMobile"
433 CUSTOM_MOBILE = "customMobile"
434 REQUEST_MOBILE = "requestMobile"
435 ERROR_MOBILE = "errorMobile"
436 SWIPE_MOBILE = "swipeMobile"
437 EVENT = "event"
438 INCIDENT = "incident"
439 CLICK_COORDINATES = "clickCoordinates"
442class PerformanceEventType(str, Enum):
443 LOCATION_DOM_COMPLETE = "domComplete"
444 LOCATION_LARGEST_CONTENTFUL_PAINT_TIME = "largestContentfulPaintTime"
445 LOCATION_TTFB = "ttfb"
446 LOCATION_AVG_CPU_LOAD = "avgCpuLoad"
447 LOCATION_AVG_MEMORY_USAGE = "avgMemoryUsage"
448 FETCH_FAILED = "fetchFailed"
449 # fetch_duration = "FETCH_DURATION"
452class FilterType(str, Enum):
453 USER_OS = "userOs"
454 USER_BROWSER = "userBrowser"
455 USER_BROWSER_VERSION = "userBrowserVersion"
456 USER_DEVICE = "userDevice"
457 USER_DEVICE_TYPE = "userDeviceType"
458 USER_COUNTRY = "userCountry"
459 USER_CITY = "userCity"
460 USER_STATE = "userState"
461 USER_ID = "userId"
462 DISTINCT_ID = "distinct_id"
463 USER_ANONYMOUS_ID = "userAnonymousId"
464 REFERRER = "referrer"
465 REV_ID = "revId"
466 # IOS
467 USER_OS_MOBILE = "userOsIos"
468 USER_DEVICE_MOBILE = "userDeviceIos"
469 USER_COUNTRY_MOBILE = "userCountryIos"
470 USER_ID_MOBILE = "userIdIos"
471 USER_ANONYMOUS_ID_MOBILE = "userAnonymousIdIos"
472 REV_ID_MOBILE = "revIdIos"
473 #
474 DURATION = "duration"
475 PLATFORM = "platform"
476 METADATA = "metadata"
477 ISSUE = "issue"
478 EVENTS_COUNT = "eventsCount"
479 UTM_SOURCE = "utmSource"
480 UTM_MEDIUM = "utmMedium"
481 UTM_CAMPAIGN = "utmCampaign"
482 # Mobile conditions
483 THERMAL_STATE = "thermalState"
484 MAIN_THREAD_CPU = "mainThreadCPU"
485 VIEW_COMPONENT = "viewComponent"
486 LOG_EVENT = "logEvent"
487 CLICK_EVENT = "clickEvent"
488 MEMORY_USAGE = "memoryUsage"
491class SearchEventOperator(str, Enum):
492 IS = "is"
493 IS_ANY = "isAny"
494 ON = "on"
495 ON_ANY = "onAny"
496 IS_NOT = "isNot"
497 IS_UNDEFINED = "isUndefined"
498 NOT_ON = "notOn"
499 CONTAINS = "contains"
500 NOT_CONTAINS = "notContains"
501 STARTS_WITH = "startsWith"
502 ENDS_WITH = "endsWith"
503 PATTERN = "regex"
506class ClickEventExtraOperator(str, Enum):
507 IS = "selectorIs"
508 IS_ANY = "selectorIsAny"
509 IS_NOT = "selectorIsNot"
510 IS_UNDEFINED = "selectorIsUndefined"
511 CONTAINS = "selectorContains"
512 NOT_CONTAINS = "selectorNotContains"
513 STARTS_WITH = "selectorStartsWith"
514 ENDS_WITH = "selectorEndsWith"
517class PlatformType(str, Enum):
518 MOBILE = "mobile"
519 DESKTOP = "desktop"
520 TABLET = "tablet"
523class SearchEventOrder(str, Enum):
524 THEN = "then"
525 OR = "or"
526 AND = "and"
529class IssueType(str, Enum):
530 CLICK_RAGE = "click_rage"
531 DEAD_CLICK = "dead_click"
532 EXCESSIVE_SCROLLING = "excessive_scrolling"
533 BAD_REQUEST = "bad_request"
534 MISSING_RESOURCE = "missing_resource"
535 MEMORY = "memory"
536 CPU = "cpu"
537 SLOW_RESOURCE = "slow_resource"
538 SLOW_PAGE_LOAD = "slow_page_load"
539 CRASH = "crash"
540 CUSTOM = "custom"
541 JS_EXCEPTION = "js_exception"
542 MOUSE_THRASHING = "mouse_thrashing"
543 TAP_RAGE = "tap_rage" # IOS
544 INCIDENT = "incident"
547class MetricFormatType(str, Enum):
548 SESSION_COUNT = "sessionCount"
551class MetricExtendedFormatType(str, Enum):
552 SESSION_COUNT = "sessionCount"
553 USER_COUNT = "userCount"
556class FetchFilterType(str, Enum):
557 FETCH_URL = "fetchUrl"
558 FETCH_STATUS_CODE = "fetchStatusCode"
559 FETCH_METHOD = "fetchMethod"
560 FETCH_DURATION = "fetchDuration"
561 FETCH_REQUEST_BODY = "fetchRequestBody"
562 FETCH_RESPONSE_BODY = "fetchResponseBody"
565class GraphqlFilterType(str, Enum):
566 GRAPHQL_NAME = "graphqlName"
567 GRAPHQL_METHOD = "graphqlMethod"
568 GRAPHQL_REQUEST_BODY = "graphqlRequestBody"
569 GRAPHQL_RESPONSE_BODY = "graphqlResponseBody"
572class RequestGraphqlFilterSchema(BaseModel):
573 type: Union[FetchFilterType, GraphqlFilterType] = Field(...)
574 value: List[Union[int, str]] = Field(...)
575 operator: Annotated[
576 Union[SearchEventOperator, MathOperator], AfterValidator(check_regex)
577 ] = Field(...)
579 @model_validator(mode="before")
580 @classmethod
581 def _transform_data(cls, values):
582 if values.get("type") in [
583 FetchFilterType.FETCH_DURATION,
584 FetchFilterType.FETCH_STATUS_CODE,
585 ]:
586 values["value"] = [
587 int(v) for v in values["value"] if v is not None and str(v).isnumeric()
588 ]
589 return values
592class EventPredefinedPropertyType(str, Enum):
593 TIME = "$time"
594 SOURCE = "$source"
595 DURATION_S = "$duration_s"
596 DESCRIPTION = "description"
597 AUTO_CAPTURED = "$auto_captured"
598 SDK_EDITION = "$sdk_edition"
599 SDK_VERSION = "$sdk_version"
600 DEVICE_ID = "$device_id"
601 OS = "$os"
602 OS_VERSION = "$os_version"
603 BROWSER = "$browser"
604 BROWSER_VERSION = "$browser_version"
605 DEVICE = "$device"
606 SCREEN_HEIGHT = "$screen_height"
607 SCREEN_WIDTH = "$screen_width"
608 CURRENT_URL = "$current_url"
609 INITIAL_REFERRER = "$initial_referrer"
610 REFERRING_DOMAIN = "$referring_domain"
611 REFERRER = "$referrer"
612 INITIAL_REFERRING_DOMAIN = "$initial_referring_domain"
613 SEARCH_ENGINE = "$search_engine"
614 SEARCH_ENGINE_KEYWORD = "$search_engine_keyword"
615 UTM_SOURCE = "utm_source"
616 UTM_MEDIUM = "utm_medium"
617 UTM_CAMPAIGN = "utm_campaign"
618 COUNTRY = "$country"
619 STATE = "$state"
620 CITY = "$city"
621 ISSUE_TYPE = "issue_type"
622 TAGS = "$tags"
623 IMPORT = "$import"
626class PropertyType(str, Enum):
627 INT = "int"
628 FLOAT = "float"
629 DATETIME = "datetime"
630 STRING = "string"
631 ARRAY = "array"
632 TUPLE = "tuple"
633 MAP = "map"
634 NESTED = "nested"
637class PropertyFilterSchema(BaseModel):
638 is_event: Literal[False] = False
639 name: Union[EventPredefinedPropertyType, str] = Field(...)
640 operator: Union[SearchEventOperator, MathOperator] = Field(...)
641 value: List[Union[int, str]] = Field(...)
642 data_type: PropertyType = Field(default=PropertyType.STRING.value)
643 auto_captured: Optional[bool] = Field(default=True)
645 # property_type: Optional[Literal["string", "number", "date"]] = Field(default=None)
647 @computed_field
648 @property
649 def is_predefined(self) -> bool:
650 return EventPredefinedPropertyType.has_value(self.name)
652 @model_validator(mode="after")
653 def transform_name(self):
654 if isinstance(self.name, Enum):
655 self.name = self.name.value
656 # name is interpolated into ClickHouse SQL as a column identifier,
657 # so restrict it to a safe character set to prevent SQL injection.
658 check_property_name(self.name)
659 return self
661 @model_validator(mode="after")
662 def _check_regex_value(self):
663 if self.operator == SearchEventOperator.PATTERN:
664 for v in self.value:
665 check_regex(v)
666 return self
669class EventPropertiesSchema(BaseModel):
670 operator: Literal["and", "or"] = Field(...)
671 filters: List[PropertyFilterSchema] = Field(...)
674class SessionSearchEventSchema(BaseModel):
675 is_event: Literal[True] = True
676 value: List[Union[str, int]] = Field(...)
677 name: Union[EventType, PerformanceEventType, str] = Field(...)
678 operator: Union[SearchEventOperator, ClickEventExtraOperator] = Field(...)
679 source: Optional[List[Union[ErrorSource, int, str]]] = Field(default=None)
680 sourceOperator: Optional[MathOperator] = Field(default=None)
681 auto_captured: Optional[bool] = Field(default=True)
682 filters: Optional[List[RequestGraphqlFilterSchema]] = Field(default_factory=list)
683 properties: Optional[EventPropertiesSchema] = Field(default=None)
685 _remove_duplicate_values = field_validator("value", mode="before")(
686 remove_duplicate_values
687 )
688 _single_to_list_values = field_validator("value", mode="before")(single_to_list)
690 @model_validator(mode="after")
691 def event_validator(self):
692 if isinstance(self.name, PerformanceEventType):
693 if self.name == PerformanceEventType.FETCH_FAILED:
694 return self
696 assert self.sourceOperator is not None, (
697 "sourceOperator should not be null for PerformanceEventType"
698 )
699 assert self.source is not None, f"source is required for {self.name}"
700 assert isinstance(self.source, list), (
701 f"source of type list is required for {self.name}"
702 )
703 for c in self.source:
704 assert isinstance(c, int), (
705 f"source value should be of type int for {self.name}"
706 )
707 elif self.name == EventType.ERROR and self.source is None:
708 self.source = [ErrorSource.JS_EXCEPTION]
709 elif self.name == EventType.REQUEST_DETAILS:
710 assert isinstance(self.filters, List) and len(self.filters) > 0, (
711 f"filters should be defined for {EventType.REQUEST_DETAILS}"
712 )
713 elif self.name == EventType.GRAPHQL:
714 assert isinstance(self.filters, List) and len(self.filters) > 0, (
715 f"filters should be defined for {EventType.GRAPHQL}"
716 )
717 elif self.name == EventType.CLICK_COORDINATES:
718 assert isinstance(self.value, List) and (
719 len(self.value) == 0 or len(self.value) == 2 or len(self.value) == 4
720 ), (
721 f"value should be [x,y] or [x1,x2,y1,y2] for {EventType.CLICK_COORDINATES}"
722 )
724 if isinstance(self.operator, ClickEventExtraOperator):
725 assert self.name == EventType.CLICK, (
726 f"operator:{self.operator} is only available for event-name: {EventType.CLICK}"
727 )
728 return self
730 @model_validator(mode="after")
731 def _check_regex_value(self):
732 if self.operator == SearchEventOperator.PATTERN:
733 for v in self.value:
734 check_regex(v)
735 return self
738class SessionSearchFilterSchema(BaseModel):
739 is_event: Literal[False] = False
740 value: List[Union[IssueType, PlatformType, int, str]] = Field(default_factory=list)
741 name: Union[FilterType, str] = Field(...)
742 operator: Union[SearchEventOperator, MathOperator] = Field(...)
743 source: Optional[Union[ErrorSource, str]] = Field(default=None)
744 # used for global-properties
745 data_type: Optional[PropertyType] = Field(default=PropertyType.STRING.value)
746 # used to tell if the current filter is predefined or user-property having the same name
747 # used in case the user sends a property with the same name aas a predefined property
748 auto_captured: Optional[bool] = Field(default=True)
750 _remove_duplicate_values = field_validator("value", mode="before")(
751 remove_duplicate_values
752 )
753 _single_to_list_values = field_validator("value", mode="before")(single_to_list)
755 @computed_field
756 @property
757 def is_predefined(self) -> bool:
758 return FilterType.has_value(self.name)
760 @model_validator(mode="before")
761 @classmethod
762 def _transform_data(cls, values):
763 if values.get("source") is not None:
764 if isinstance(values["source"], list):
765 if len(values["source"]) == 0:
766 values["source"] = None
767 elif len(values["source"]) == 1:
768 values["source"] = values["source"][0]
769 else:
770 raise ValueError(f"Unsupported multi-values source")
771 return values
773 @model_validator(mode="after")
774 def filter_validator(self):
775 if self.name == FilterType.METADATA:
776 assert self.source is not None and len(self.source) > 0, (
777 "must specify a valid 'source' for metadata filter"
778 )
779 elif self.name == FilterType.ISSUE:
780 for i, v in enumerate(self.value):
781 if IssueType.has_value(v):
782 self.value[i] = IssueType(v)
783 else:
784 raise ValueError(
785 f"value should be of type IssueType for {self.name} filter"
786 )
787 elif self.name == FilterType.PLATFORM:
788 for i, v in enumerate(self.value):
789 if PlatformType.has_value(v):
790 self.value[i] = PlatformType(v)
791 else:
792 raise ValueError(
793 f"value should be of type PlatformType for {self.name} filter"
794 )
795 elif self.name == FilterType.EVENTS_COUNT:
796 if MathOperator.has_value(self.operator):
797 self.operator = MathOperator(self.operator)
798 else:
799 raise ValueError(
800 f"operator should be of type MathOperator for {self.name} filter"
801 )
803 for v in self.value:
804 assert isinstance(v, int), (
805 f"value should be of type int for {self.name} filter"
806 )
807 else:
808 if SearchEventOperator.has_value(self.operator):
809 self.operator = SearchEventOperator(self.operator)
810 elif MathOperator.has_value(self.operator):
811 self.operator = MathOperator(self.operator)
812 else:
813 raise ValueError(
814 f"operator should be of type SearchEventOperator or MathOperator for {self.name} filter"
815 )
817 return self
819 @model_validator(mode="after")
820 def _check_regex_value(self):
821 if self.operator == SearchEventOperator.PATTERN:
822 for v in self.value:
823 check_regex(v)
824 return self
827class _PaginatedSchema(BaseModel):
828 limit: int = Field(default=200, gt=0, le=200)
829 page: int = Field(default=1, gt=0)
832class SortOrderType(str, Enum):
833 ASC = "ASC"
834 DESC = "DESC"
837def add_missing_is_event(values: dict):
838 if values.get("isEvent") is None:
839 values["isEvent"] = (
840 EventType.has_value(values["name"])
841 or PerformanceEventType.has_value(values["name"])
842 or ProductAnalyticsSelectedEventType.has_value(values["name"])
843 )
844 return values
847# this type is created to allow mixing events&filters and specifying a discriminator
848GroupedFilterType = Annotated[
849 Union[SessionSearchFilterSchema, SessionSearchEventSchema],
850 Field(discriminator="is_event"),
851 BeforeValidator(add_missing_is_event),
852]
855class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
856 events: List[SessionSearchEventSchema] = Field(
857 default_factory=list, doc_hidden=True
858 )
859 filters: List[GroupedFilterType] = Field(default_factory=list)
860 sort: Literal["startTs", "eventsCount"] = Field(default="startTs")
861 order: SortOrderType = Field(default=SortOrderType.DESC)
862 events_order: Optional[SearchEventOrder] = Field(default=SearchEventOrder.THEN)
863 group_by_user: bool = Field(default=False)
864 bookmarked: bool = Field(default=False)
866 @model_validator(mode="before")
867 @classmethod
868 def transform_order(cls, values):
869 if values.get("sort") is None:
870 values["sort"] = "startTs"
872 if values.get("order") is None:
873 values["order"] = SortOrderType.DESC
874 else:
875 values["order"] = values["order"].upper()
876 return values
878 @model_validator(mode="before")
879 @classmethod
880 def add_missing_attributes(cls, values):
881 # in case isEvent is wrong:
882 for f in values.get("filters") or []:
883 if f.get("name") is None:
884 continue
885 if EventType.has_value(f["name"]) and not f.get("isEvent"):
886 f["isEvent"] = True
887 elif FilterType.has_value(f["name"]) and f.get("isEvent"):
888 f["isEvent"] = False
890 # in case the old search payload was passed
891 for v in values.get("events") or []:
892 v["isEvent"] = True
894 return values
896 @model_validator(mode="before")
897 @classmethod
898 def remove_wrong_filter_values(cls, values):
899 for f in values.get("filters", []):
900 if not isinstance(f, dict):
901 continue
902 vals = []
903 for v in f.get("value", []):
904 if f.get("name", "") == FilterType.DURATION.value and v is None:
905 v = 0
906 if v is not None and (
907 f.get("name", "") != FilterType.DURATION.value or str(v).isnumeric()
908 ):
909 vals.append(v)
910 f["value"] = vals
911 return values
913 @model_validator(mode="after")
914 def check_pa_event_filter(self):
915 for v in self.filters + self.events:
916 if v.name == EventType.EVENT:
917 assert v.operator in (SearchEventOperator.IS, MathOperator.EQUAL), (
918 "operator must be {SearchEventOperator.IS} or {MathOperator.EQUAL} for EVENT type"
919 )
920 assert len(v.value) == 1, (
921 "value must have 1 single value for EVENT type"
922 )
923 return self
925 @model_validator(mode="after")
926 def split_filters_events(self):
927 n_filters = []
928 n_events = []
929 for v in self.filters:
930 if v.is_event:
931 n_events.append(v)
932 else:
933 n_filters.append(v)
934 self.events = n_events
935 self.filters = n_filters
936 return self
938 @field_validator("filters", mode="after")
939 @classmethod
940 def merge_identical_filters(cls, values):
941 # ignore 'issue' type as it could be used for step-filters and tab-filters at the same time
942 i = 0
943 while i < len(values):
944 if values[i].is_event or values[i].name == FilterType.ISSUE:
945 if values[i].name == FilterType.ISSUE:
946 values[i] = remove_duplicate_values(values[i])
947 i += 1
948 continue
949 j = i + 1
950 while j < len(values):
951 if (
952 values[i].name == values[j].name
953 and values[i].operator == values[j].operator
954 and (
955 values[i].name != FilterType.METADATA
956 or values[i].source == values[j].source
957 )
958 ):
959 values[i].value += values[j].value
960 del values[j]
961 else:
962 j += 1
963 values[i] = remove_duplicate_values(values[i])
964 i += 1
966 return values
969class ErrorStatus(str, Enum):
970 ALL = "all"
971 UNRESOLVED = "unresolved"
972 RESOLVED = "resolved"
973 IGNORED = "ignored"
976class ErrorSort(str, Enum):
977 LAST_OCCURRENCE = "lastOccurrence"
978 FIRST_OCCURRENCE = "firstOccurrence"
979 USERS_COUNT = "users"
980 SESSIONS_COUNT = "sessions"
983class SearchErrorsSchema(SessionsSearchPayloadSchema):
984 sort: ErrorSort = Field(default=ErrorSort.LAST_OCCURRENCE)
985 density: Optional[int] = Field(default=7)
986 status: Optional[ErrorStatus] = Field(default=ErrorStatus.ALL)
987 query: Optional[str] = Field(default=None)
990class ProductAnalyticsSelectedEventType(str, Enum):
991 CLICK = EventType.CLICK.value
992 INPUT = EventType.INPUT.value
993 LOCATION = EventType.LOCATION.value
994 CUSTOM_EVENT = EventType.CUSTOM.value
997class MobileSignPayloadSchema(BaseModel):
998 keys: List[str] = Field(...)
1001class ProjectConditions(BaseModel):
1002 condition_id: Optional[int] = Field(default=None)
1003 name: str = Field(...)
1004 capture_rate: int = Field(..., ge=0, le=100)
1005 filters: List[GroupedFilterType] = Field(default_factory=list)
1008class ProjectSettings(BaseModel):
1009 rate: int = Field(..., ge=0, le=100)
1010 conditional_capture: bool = Field(default=False)
1011 conditions: List[ProjectConditions] = Field(default_factory=list)
1014class TemplatePredefinedUnits(str, Enum):
1015 MILLISECOND = "ms"
1016 SECOND = "s"
1017 MINUTE = "min"
1018 MEMORY = "mb"
1019 FRAME = "f/s"
1020 PERCENTAGE = "%"
1021 COUNT = "count"
1024class LiveFilterType(str, Enum):
1025 USER_OS = FilterType.USER_OS.value
1026 USER_BROWSER = FilterType.USER_BROWSER.value
1027 USER_DEVICE = FilterType.USER_DEVICE.value
1028 USER_COUNTRY = FilterType.USER_COUNTRY.value
1029 USER_CITY = FilterType.USER_CITY.value
1030 USER_STATE = FilterType.USER_STATE.value
1031 USER_ID = FilterType.USER_ID.value
1032 USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value
1033 REV_ID = FilterType.REV_ID.value
1034 PLATFORM = FilterType.PLATFORM.value
1035 PAGE_TITLE = "pageTitle"
1036 SESSION_ID = "sessionId"
1037 METADATA = FilterType.METADATA.value
1038 USER_UUID = "userUuid"
1039 TRACKER_VERSION = "trackerVersion"
1040 USER_BROWSER_VERSION = "userBrowserVersion"
1041 USER_DEVICE_TYPE = "userDeviceType"
1044class LiveSessionSearchFilterSchema(BaseModel):
1045 value: Union[List[str], str] = Field(...)
1046 name: LiveFilterType = Field(...)
1047 source: Optional[str] = Field(default=None)
1048 operator: Literal[SearchEventOperator.IS, SearchEventOperator.CONTAINS] = Field(
1049 default=SearchEventOperator.CONTAINS
1050 )
1052 @model_validator(mode="after")
1053 def __validator(self):
1054 if self.name is not None and self.name == LiveFilterType.METADATA:
1055 assert self.source is not None, (
1056 "source should not be null for METADATA type"
1057 )
1058 assert len(self.source) > 0, "source should not be empty for METADATA type"
1059 return self
1061 @model_validator(mode="after")
1062 def _check_regex_value(self):
1063 if self.operator == SearchEventOperator.PATTERN: 1063 ↛ 1064line 1063 didn't jump to line 1064 because the condition on line 1063 was never true
1064 for v in self.value:
1065 check_regex(v)
1066 return self
1069class LiveSessionsSearchPayloadSchema(_PaginatedSchema):
1070 filters: List[LiveSessionSearchFilterSchema] = Field([])
1071 sort: Union[LiveFilterType, str] = Field(default="TIMESTAMP")
1072 order: SortOrderType = Field(default=SortOrderType.DESC)
1074 @model_validator(mode="before")
1075 @classmethod
1076 def __transform(cls, values):
1077 if values.get("order") is not None:
1078 values["order"] = values["order"].upper()
1079 if values.get("filters") is not None:
1080 i = 0
1081 while i < len(values["filters"]):
1082 if (
1083 values["filters"][i]["value"] is None
1084 or len(values["filters"][i]["value"]) == 0
1085 ):
1086 del values["filters"][i]
1087 else:
1088 i += 1
1089 for i in values["filters"]:
1090 if i.get("name") == LiveFilterType.PLATFORM: 1090 ↛ 1091line 1090 didn't jump to line 1091 because the condition on line 1090 was never true
1091 i["name"] = LiveFilterType.USER_DEVICE_TYPE
1092 if values.get("sort") is not None:
1093 if values["sort"].lower() == "startts": 1093 ↛ 1094line 1093 didn't jump to line 1094 because the condition on line 1093 was never true
1094 values["sort"] = "TIMESTAMP"
1095 return values
1098class IntegrationType(str, Enum):
1099 GITHUB = "GITHUB"
1100 JIRA = "JIRA"
1101 SLACK = "SLACK"
1102 MS_TEAMS = "MSTEAMS"
1103 SENTRY = "SENTRY"
1104 BUGSNAG = "BUGSNAG"
1105 ROLLBAR = "ROLLBAR"
1106 ELASTICSEARCH = "ELASTICSEARCH"
1107 DATADOG = "DATADOG"
1108 SUMOLOGIC = "SUMOLOGIC"
1109 STACKDRIVER = "STACKDRIVER"
1110 CLOUDWATCH = "CLOUDWATCH"
1111 NEWRELIC = "NEWRELIC"
1112 DYNATRACE = "DYNATRACE"
1115class WebhookType(str, Enum):
1116 WEBHOOK = "webhook"
1117 SLACK = "slack"
1118 EMAIL = "email"
1119 MSTEAMS = "msteams"
1122class ModuleType(str, Enum):
1123 ASSIST = "assist"
1124 NOTES = "notes"
1125 BUG_REPORTS = "bug-reports"
1126 OFFLINE_RECORDINGS = "offline-recordings"
1127 ALERTS = "alerts"
1128 ASSIST_STATS = "assist-stats"
1129 RECOMMENDATIONS = "recommendations"
1130 FEATURE_FLAGS = "feature-flags"
1131 USABILITY_TESTS = "usability-tests"
1134class ModuleStatus(BaseModel):
1135 module: ModuleType = Field(...)
1136 status: bool = Field(...)
1139class TagUpdate(BaseModel):
1140 name: str = Field(..., min_length=1, max_length=100, pattern='^[a-zA-Z0-9" -]*$')
1143class TagCreate(TagUpdate):
1144 selector: str = Field(..., min_length=1, max_length=255)
1145 ignoreClickRage: bool = Field(default=False)
1146 ignoreDeadClick: bool = Field(default=False)
1149class ScopeSchema(BaseModel):
1150 scope: int = Field(default=1, ge=1, le=2)
1153class SessionModel(BaseModel):
1154 duration: int
1155 errorsCount: int
1156 eventsCount: int
1157 issueTypes: List[IssueType] = Field(default=[])
1158 metadata: dict = Field(default={})
1159 pagesCount: int
1160 platform: str
1161 projectId: int
1162 sessionId: str
1163 startTs: int
1164 timezone: Optional[str]
1165 userAnonymousId: Optional[str]
1166 userBrowser: str
1167 userCity: str
1168 userCountry: str
1169 userDevice: Optional[str]
1170 userDeviceType: str
1171 userId: Optional[str]
1172 userOs: str
1173 userState: str
1174 userUuid: str
1175 viewed: bool = Field(default=False)