* refactor(chalice): upgraded dependencies * refactor(chalice): upgraded dependencies feat(chalice): support heatmaps * fix(chalice): fixed Math-operators validation refactor(chalice): search for sessions that have events for heatmaps * refactor(chalice): search for sessions that have at least 1 location event for heatmaps * refactor(chalice): upgraded dependencies * refactor(chalice): upgraded dependencies feat(chalice): support heatmaps * fix(chalice): fixed Math-operators validation refactor(chalice): search for sessions that have events for heatmaps * refactor(chalice): search for sessions that have at least 1 location event for heatmaps * refactor(chalice): upgraded dependencies refactor(crons): upgraded dependencies refactor(alerts): upgraded dependencies * feat(chalice): get top 10 values for autocomplete CH * refactor(chalice): cleaned code refactor(chalice): upgraded dependencies refactor(alerts): upgraded dependencies refactor(crons): upgraded dependencies * feat(chalice): autocomplete return top 10 with stats * fix(chalice): fixed autocomplete top 10 meta-filters * refactor(chalice): ignore admin privilege and role for Spot-scope refactor(chalice): upgraded dependencies refactor(alerts): upgraded dependencies refactor(crons): upgraded dependencies
1662 lines
56 KiB
Python
1662 lines
56 KiB
Python
from typing import Annotated, Any
|
|
from typing import Optional, List, Union, Literal
|
|
|
|
from pydantic import Field, EmailStr, HttpUrl, SecretStr, AnyHttpUrl, validator
|
|
from pydantic import field_validator, model_validator, computed_field
|
|
|
|
from chalicelib.utils.TimeUTC import TimeUTC
|
|
from .overrides import BaseModel, Enum, ORUnion
|
|
from .transformers_validators import transform_email, remove_whitespace, remove_duplicate_values, single_to_list, \
|
|
force_is_event, NAME_PATTERN, int_to_string
|
|
from pydantic.functional_validators import BeforeValidator
|
|
|
|
|
|
def transform_old_filter_type(cls, values):
|
|
if values.get("type") is None:
|
|
return values
|
|
values["type"] = {
|
|
# filters
|
|
"USEROS": FilterType.USER_OS.value,
|
|
"USERBROWSER": FilterType.USER_BROWSER.value,
|
|
"USERDEVICE": FilterType.USER_DEVICE.value,
|
|
"USERCOUNTRY": FilterType.USER_COUNTRY.value,
|
|
"USERID": FilterType.USER_ID.value,
|
|
"USERANONYMOUSID": FilterType.USER_ANONYMOUS_ID.value,
|
|
"REFERRER": FilterType.REFERRER.value,
|
|
"REVID": FilterType.REV_ID.value,
|
|
"USEROS_IOS": FilterType.USER_OS_MOBILE.value,
|
|
"USERDEVICE_IOS": FilterType.USER_DEVICE_MOBILE.value,
|
|
"USERCOUNTRY_IOS": FilterType.USER_COUNTRY_MOBILE.value,
|
|
"USERID_IOS": FilterType.USER_ID_MOBILE.value,
|
|
"USERANONYMOUSID_IOS": FilterType.USER_ANONYMOUS_ID_MOBILE.value,
|
|
"REVID_IOS": FilterType.REV_ID_MOBILE.value,
|
|
"DURATION": FilterType.DURATION.value,
|
|
"PLATFORM": FilterType.PLATFORM.value,
|
|
"METADATA": FilterType.METADATA.value,
|
|
"ISSUE": FilterType.ISSUE.value,
|
|
"EVENTS_COUNT": FilterType.EVENTS_COUNT.value,
|
|
"UTM_SOURCE": FilterType.UTM_SOURCE.value,
|
|
"UTM_MEDIUM": FilterType.UTM_MEDIUM.value,
|
|
"UTM_CAMPAIGN": FilterType.UTM_CAMPAIGN.value,
|
|
# events:
|
|
"CLICK": EventType.CLICK.value,
|
|
"INPUT": EventType.INPUT.value,
|
|
"LOCATION": EventType.LOCATION.value,
|
|
"CUSTOM": EventType.CUSTOM.value,
|
|
"REQUEST": EventType.REQUEST.value,
|
|
"FETCH": EventType.REQUEST_DETAILS.value,
|
|
"GRAPHQL": EventType.GRAPHQL.value,
|
|
"STATEACTION": EventType.STATE_ACTION.value,
|
|
"ERROR": EventType.ERROR.value,
|
|
"CLICK_IOS": EventType.CLICK_MOBILE.value,
|
|
"INPUT_IOS": EventType.INPUT_MOBILE.value,
|
|
"VIEW_IOS": EventType.VIEW_MOBILE.value,
|
|
"CUSTOM_IOS": EventType.CUSTOM_MOBILE.value,
|
|
"REQUEST_IOS": EventType.REQUEST_MOBILE.value,
|
|
"ERROR_IOS": EventType.ERROR_MOBILE.value,
|
|
"DOM_COMPLETE": PerformanceEventType.LOCATION_DOM_COMPLETE.value,
|
|
"LARGEST_CONTENTFUL_PAINT_TIME": PerformanceEventType.LOCATION_LARGEST_CONTENTFUL_PAINT_TIME.value,
|
|
"TTFB": PerformanceEventType.LOCATION_TTFB.value,
|
|
"AVG_CPU_LOAD": PerformanceEventType.LOCATION_AVG_CPU_LOAD.value,
|
|
"AVG_MEMORY_USAGE": PerformanceEventType.LOCATION_AVG_MEMORY_USAGE.value,
|
|
"FETCH_FAILED": PerformanceEventType.FETCH_FAILED.value,
|
|
}.get(values["type"], values["type"])
|
|
return values
|
|
|
|
|
|
class _GRecaptcha(BaseModel):
|
|
g_recaptcha_response: Optional[str] = Field(default=None, alias='g-recaptcha-response')
|
|
|
|
|
|
class UserLoginSchema(_GRecaptcha):
|
|
email: EmailStr = Field(...)
|
|
password: SecretStr = Field(...)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
|
|
|
|
class UserSignupSchema(UserLoginSchema):
|
|
fullname: str = Field(..., min_length=1, pattern=NAME_PATTERN)
|
|
organizationName: str = Field(..., min_length=1, pattern=NAME_PATTERN)
|
|
|
|
_transform_fullname = field_validator('fullname', mode='before')(remove_whitespace)
|
|
_transform_organizationName = field_validator('organizationName', mode='before')(remove_whitespace)
|
|
|
|
|
|
class EditAccountSchema(BaseModel):
|
|
name: Optional[str] = Field(default=None, pattern=NAME_PATTERN)
|
|
tenantName: Optional[str] = Field(default=None, pattern=NAME_PATTERN)
|
|
opt_out: Optional[bool] = Field(default=None)
|
|
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
_transform_tenantName = field_validator('tenantName', mode='before')(remove_whitespace)
|
|
|
|
|
|
class ForgetPasswordPayloadSchema(_GRecaptcha):
|
|
email: EmailStr = Field(...)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
|
|
|
|
class EditUserPasswordSchema(BaseModel):
|
|
old_password: SecretStr = Field(...)
|
|
new_password: SecretStr = Field(...)
|
|
|
|
|
|
class CreateProjectSchema(BaseModel):
|
|
name: str = Field(default="my first project", pattern=NAME_PATTERN)
|
|
platform: Literal["web", "ios"] = Field(default="web")
|
|
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
|
|
|
|
class CurrentProjectContext(BaseModel):
|
|
project_id: int = Field(..., gt=0)
|
|
project_key: str = Field(...)
|
|
name: str = Field(...)
|
|
platform: Literal["web", "ios"] = Field(...)
|
|
|
|
|
|
class CurrentAPIContext(BaseModel):
|
|
tenant_id: int = Field(...)
|
|
project: Optional[CurrentProjectContext] = Field(default=None)
|
|
|
|
|
|
class CurrentContext(CurrentAPIContext):
|
|
user_id: int = Field(...)
|
|
email: EmailStr = Field(...)
|
|
role: str = Field(...)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
|
|
@computed_field
|
|
@property
|
|
def is_owner(self) -> bool:
|
|
return self.role == "owner"
|
|
|
|
@computed_field
|
|
@property
|
|
def is_admin(self) -> bool:
|
|
return self.role == "admin"
|
|
|
|
@computed_field
|
|
@property
|
|
def is_member(self) -> bool:
|
|
return self.role == "member"
|
|
|
|
|
|
class AddCollaborationSchema(BaseModel):
|
|
name: str = Field(..., pattern=NAME_PATTERN)
|
|
url: HttpUrl = Field(...)
|
|
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
_transform_url = field_validator('url', mode='before')(remove_whitespace)
|
|
|
|
|
|
class EditCollaborationSchema(AddCollaborationSchema):
|
|
name: Optional[str] = Field(default=None, pattern=NAME_PATTERN)
|
|
|
|
|
|
class _TimedSchema(BaseModel):
|
|
startTimestamp: int = Field(default=None)
|
|
endTimestamp: int = Field(default=None)
|
|
|
|
@model_validator(mode='before')
|
|
def transform_time(cls, values):
|
|
if values.get("startTimestamp") is None and values.get("startDate") is not None:
|
|
values["startTimestamp"] = values["startDate"]
|
|
if values.get("endTimestamp") is None and values.get("endDate") is not None:
|
|
values["endTimestamp"] = values["endDate"]
|
|
return values
|
|
|
|
@model_validator(mode='after')
|
|
def __time_validator(cls, values):
|
|
if values.startTimestamp is not None:
|
|
assert 0 <= values.startTimestamp, "startTimestamp must be greater or equal to 0"
|
|
if values.endTimestamp is not None:
|
|
assert 0 <= values.endTimestamp, "endTimestamp must be greater or equal to 0"
|
|
if values.startTimestamp is not None and values.endTimestamp is not None:
|
|
assert values.startTimestamp <= values.endTimestamp, \
|
|
"endTimestamp must be greater or equal to startTimestamp"
|
|
return values
|
|
|
|
|
|
class NotificationsViewSchema(_TimedSchema):
|
|
ids: List[int] = Field(default=[])
|
|
startTimestamp: Optional[int] = Field(default=None)
|
|
endTimestamp: Optional[int] = Field(default=None)
|
|
|
|
|
|
class IssueTrackingIntegration(BaseModel):
|
|
token: str = Field(...)
|
|
|
|
|
|
class IssueTrackingGithubSchema(IssueTrackingIntegration):
|
|
pass
|
|
|
|
|
|
class IssueTrackingJiraSchema(IssueTrackingIntegration):
|
|
username: str = Field(...)
|
|
url: HttpUrl = Field(...)
|
|
|
|
@field_validator('url')
|
|
@classmethod
|
|
def transform_url(cls, v: HttpUrl):
|
|
return HttpUrl.build(scheme=v.scheme.lower(), host=v.host.lower())
|
|
|
|
|
|
class WebhookSchema(BaseModel):
|
|
webhook_id: Optional[int] = Field(default=None)
|
|
endpoint: AnyHttpUrl = Field(...)
|
|
auth_header: Optional[str] = Field(default=None)
|
|
name: str = Field(default="", max_length=100, pattern=NAME_PATTERN)
|
|
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
|
|
|
|
class CreateMemberSchema(BaseModel):
|
|
user_id: Optional[int] = Field(default=None)
|
|
name: str = Field(...)
|
|
email: EmailStr = Field(...)
|
|
admin: Optional[bool] = Field(default=False)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
|
|
|
|
class EditMemberSchema(BaseModel):
|
|
name: str = Field(..., pattern=NAME_PATTERN)
|
|
email: EmailStr = Field(...)
|
|
admin: bool = Field(default=False)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
_transform_name = field_validator('name', mode='before')(remove_whitespace)
|
|
|
|
|
|
class EditPasswordByInvitationSchema(BaseModel):
|
|
invitation: str = Field(...)
|
|
passphrase: str = Field(..., alias="pass")
|
|
password: SecretStr = Field(...)
|
|
|
|
|
|
class AssignmentSchema(BaseModel):
|
|
assignee: str = Field(...)
|
|
description: str = Field(...)
|
|
title: str = Field(...)
|
|
issue_type: str = Field(...)
|
|
|
|
_transform_title = field_validator('title', mode='before')(remove_whitespace)
|
|
|
|
|
|
class CommentAssignmentSchema(BaseModel):
|
|
message: str = Field(...)
|
|
|
|
|
|
class IntegrationNotificationSchema(BaseModel):
|
|
comment: Optional[str] = Field(default=None)
|
|
|
|
|
|
class GdprSchema(BaseModel):
|
|
maskEmails: bool = Field(...)
|
|
sampleRate: int = Field(...)
|
|
maskNumbers: bool = Field(...)
|
|
defaultInputMode: str = Field(...)
|
|
|
|
|
|
class SampleRateSchema(BaseModel):
|
|
rate: int = Field(..., ge=0, le=100)
|
|
capture_all: bool = Field(default=False)
|
|
|
|
|
|
class WeeklyReportConfigSchema(BaseModel):
|
|
weekly_report: bool = Field(default=True)
|
|
|
|
|
|
class IntegrationBase(BaseModel):
|
|
pass
|
|
|
|
|
|
class IntegrationSentrySchema(IntegrationBase):
|
|
project_slug: str = Field(...)
|
|
organization_slug: str = Field(...)
|
|
token: str = Field(...)
|
|
|
|
|
|
class IntegrationDatadogSchema(IntegrationBase):
|
|
api_key: str = Field(...)
|
|
application_key: str = Field(...)
|
|
|
|
|
|
class IntegartionStackdriverSchema(IntegrationBase):
|
|
service_account_credentials: str = Field(...)
|
|
log_name: str = Field(...)
|
|
|
|
|
|
class IntegrationNewrelicSchema(IntegrationBase):
|
|
application_id: str = Field(...)
|
|
x_query_key: str = Field(...)
|
|
region: bool = Field(default=False)
|
|
|
|
|
|
class IntegrationRollbarSchema(IntegrationBase):
|
|
access_token: str = Field(...)
|
|
|
|
|
|
class IntegrationBugsnagBasicSchema(IntegrationBase):
|
|
authorization_token: str = Field(...)
|
|
|
|
|
|
class IntegrationBugsnagSchema(IntegrationBugsnagBasicSchema):
|
|
bugsnag_project_id: str = Field(...)
|
|
|
|
|
|
class IntegrationCloudwatchBasicSchema(IntegrationBase):
|
|
aws_access_key_id: str = Field(...)
|
|
aws_secret_access_key: str = Field(...)
|
|
region: str = Field(...)
|
|
|
|
|
|
class IntegrationCloudwatchSchema(IntegrationCloudwatchBasicSchema):
|
|
log_group_name: str = Field(...)
|
|
|
|
|
|
class IntegrationElasticsearchTestSchema(IntegrationBase):
|
|
host: str = Field(...)
|
|
port: int = Field(...)
|
|
api_key_id: Optional[str] = Field(default=None)
|
|
api_key: str = Field(...)
|
|
|
|
|
|
class IntegrationElasticsearchSchema(IntegrationElasticsearchTestSchema):
|
|
indexes: str = Field(...)
|
|
|
|
|
|
class IntegrationSumologicSchema(IntegrationBase):
|
|
access_id: str = Field(...)
|
|
access_key: str = Field(...)
|
|
region: str = Field(...)
|
|
|
|
|
|
class MetadataSchema(BaseModel):
|
|
index: Optional[int] = Field(default=None)
|
|
key: str = Field(...)
|
|
|
|
_transform_key = field_validator('key', mode='before')(remove_whitespace)
|
|
|
|
|
|
class EmailPayloadSchema(BaseModel):
|
|
auth: str = Field(...)
|
|
email: EmailStr = Field(...)
|
|
link: str = Field(...)
|
|
message: str = Field(...)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
|
|
|
|
class MemberInvitationPayloadSchema(BaseModel):
|
|
auth: str = Field(...)
|
|
email: EmailStr = Field(...)
|
|
invitation_link: str = Field(...)
|
|
client_id: str = Field(...)
|
|
sender_name: str = Field(...)
|
|
|
|
_transform_email = field_validator('email', mode='before')(transform_email)
|
|
|
|
|
|
class _AlertMessageSchema(BaseModel):
|
|
type: str = Field(...)
|
|
value: str = Field(...)
|
|
|
|
_transform_value = field_validator('value', mode='before')(int_to_string)
|
|
|
|
|
|
class AlertDetectionType(str, Enum):
|
|
PERCENT = "percent"
|
|
CHANGE = "change"
|
|
|
|
|
|
class _AlertOptionSchema(BaseModel):
|
|
message: List[_AlertMessageSchema] = Field([])
|
|
currentPeriod: Literal[15, 30, 60, 120, 240, 1440] = Field(...)
|
|
previousPeriod: Literal[15, 30, 60, 120, 240, 1440] = Field(default=15)
|
|
lastNotification: Optional[int] = Field(default=None)
|
|
renotifyInterval: Optional[int] = Field(default=720)
|
|
|
|
|
|
class AlertColumn(str, Enum):
|
|
PERFORMANCE__DOM_CONTENT_LOADED__AVERAGE = "performance.dom_content_loaded.average"
|
|
PERFORMANCE__FIRST_MEANINGFUL_PAINT__AVERAGE = "performance.first_meaningful_paint.average"
|
|
PERFORMANCE__PAGE_LOAD_TIME__AVERAGE = "performance.page_load_time.average"
|
|
PERFORMANCE__DOM_BUILD_TIME__AVERAGE = "performance.dom_build_time.average"
|
|
PERFORMANCE__SPEED_INDEX__AVERAGE = "performance.speed_index.average"
|
|
PERFORMANCE__PAGE_RESPONSE_TIME__AVERAGE = "performance.page_response_time.average"
|
|
PERFORMANCE__TTFB__AVERAGE = "performance.ttfb.average"
|
|
PERFORMANCE__TIME_TO_RENDER__AVERAGE = "performance.time_to_render.average"
|
|
PERFORMANCE__IMAGE_LOAD_TIME__AVERAGE = "performance.image_load_time.average"
|
|
PERFORMANCE__REQUEST_LOAD_TIME__AVERAGE = "performance.request_load_time.average"
|
|
RESOURCES__LOAD_TIME__AVERAGE = "resources.load_time.average"
|
|
RESOURCES__MISSING__COUNT = "resources.missing.count"
|
|
ERRORS__4XX_5XX__COUNT = "errors.4xx_5xx.count"
|
|
ERRORS__4XX__COUNT = "errors.4xx.count"
|
|
ERRORS__5XX__COUNT = "errors.5xx.count"
|
|
ERRORS__JAVASCRIPT__IMPACTED_SESSIONS__COUNT = "errors.javascript.impacted_sessions.count"
|
|
PERFORMANCE__CRASHES__COUNT = "performance.crashes.count"
|
|
ERRORS__JAVASCRIPT__COUNT = "errors.javascript.count"
|
|
ERRORS__BACKEND__COUNT = "errors.backend.count"
|
|
CUSTOM = "CUSTOM"
|
|
|
|
|
|
class MathOperator(str, Enum):
|
|
EQUAL = "="
|
|
LESS = "<"
|
|
GREATER = ">"
|
|
LESS_EQ = "<="
|
|
GREATER_EQ = ">="
|
|
|
|
|
|
class _AlertQuerySchema(BaseModel):
|
|
left: Union[AlertColumn, int] = Field(...)
|
|
right: float = Field(...)
|
|
operator: MathOperator = Field(...)
|
|
|
|
|
|
class AlertDetectionMethod(str, Enum):
|
|
THRESHOLD = "threshold"
|
|
CHANGE = "change"
|
|
|
|
|
|
class AlertSchema(BaseModel):
|
|
name: str = Field(..., pattern=NAME_PATTERN)
|
|
detection_method: AlertDetectionMethod = Field(...)
|
|
change: Optional[AlertDetectionType] = Field(default=AlertDetectionType.CHANGE)
|
|
description: Optional[str] = Field(default=None)
|
|
options: _AlertOptionSchema = Field(...)
|
|
query: _AlertQuerySchema = Field(...)
|
|
series_id: Optional[int] = Field(default=None, doc_hidden=True)
|
|
|
|
@model_validator(mode="after")
|
|
def transform_alert(cls, values):
|
|
values.series_id = None
|
|
if isinstance(values.query.left, int):
|
|
values.series_id = values.query.left
|
|
values.query.left = AlertColumn.CUSTOM
|
|
|
|
return values
|
|
|
|
|
|
class SourcemapUploadPayloadSchema(BaseModel):
|
|
urls: List[str] = Field(..., alias="URL")
|
|
|
|
|
|
class ErrorSource(str, Enum):
|
|
JS_EXCEPTION = "js_exception"
|
|
BUGSNAG = "bugsnag"
|
|
CLOUDWATCH = "cloudwatch"
|
|
DATADOG = "datadog"
|
|
NEWRELIC = "newrelic"
|
|
ROLLBAR = "rollbar"
|
|
SENTRY = "sentry"
|
|
STACKDRIVER = "stackdriver"
|
|
SUMOLOGIC = "sumologic"
|
|
|
|
|
|
class EventType(str, Enum):
|
|
CLICK = "click"
|
|
INPUT = "input"
|
|
LOCATION = "location"
|
|
CUSTOM = "custom"
|
|
REQUEST = "request"
|
|
REQUEST_DETAILS = "fetch"
|
|
GRAPHQL = "graphql"
|
|
STATE_ACTION = "stateAction"
|
|
ERROR = "error"
|
|
TAG = "tag"
|
|
CLICK_MOBILE = "clickMobile"
|
|
INPUT_MOBILE = "inputMobile"
|
|
VIEW_MOBILE = "viewMobile"
|
|
CUSTOM_MOBILE = "customMobile"
|
|
REQUEST_MOBILE = "requestMobile"
|
|
ERROR_MOBILE = "errorMobile"
|
|
SWIPE_MOBILE = "swipeMobile"
|
|
|
|
|
|
class PerformanceEventType(str, Enum):
|
|
LOCATION_DOM_COMPLETE = "domComplete"
|
|
LOCATION_LARGEST_CONTENTFUL_PAINT_TIME = "largestContentfulPaintTime"
|
|
LOCATION_TTFB = "ttfb"
|
|
LOCATION_AVG_CPU_LOAD = "avgCpuLoad"
|
|
LOCATION_AVG_MEMORY_USAGE = "avgMemoryUsage"
|
|
FETCH_FAILED = "fetchFailed"
|
|
# fetch_duration = "FETCH_DURATION"
|
|
|
|
|
|
class FilterType(str, Enum):
|
|
USER_OS = "userOs"
|
|
USER_BROWSER = "userBrowser"
|
|
USER_DEVICE = "userDevice"
|
|
USER_COUNTRY = "userCountry"
|
|
USER_CITY = "userCity"
|
|
USER_STATE = "userState"
|
|
USER_ID = "userId"
|
|
USER_ANONYMOUS_ID = "userAnonymousId"
|
|
REFERRER = "referrer"
|
|
REV_ID = "revId"
|
|
# IOS
|
|
USER_OS_MOBILE = "userOsIos"
|
|
USER_DEVICE_MOBILE = "userDeviceIos"
|
|
USER_COUNTRY_MOBILE = "userCountryIos"
|
|
USER_ID_MOBILE = "userIdIos"
|
|
USER_ANONYMOUS_ID_MOBILE = "userAnonymousIdIos"
|
|
REV_ID_MOBILE = "revIdIos"
|
|
#
|
|
DURATION = "duration"
|
|
PLATFORM = "platform"
|
|
METADATA = "metadata"
|
|
ISSUE = "issue"
|
|
EVENTS_COUNT = "eventsCount"
|
|
UTM_SOURCE = "utmSource"
|
|
UTM_MEDIUM = "utmMedium"
|
|
UTM_CAMPAIGN = "utmCampaign"
|
|
# Mobile conditions
|
|
THERMAL_STATE = "thermalState"
|
|
MAIN_THREAD_CPU = "mainThreadCPU"
|
|
VIEW_COMPONENT = "viewComponent"
|
|
LOG_EVENT = "logEvent"
|
|
CLICK_EVENT = "clickEvent"
|
|
MEMORY_USAGE = "memoryUsage"
|
|
|
|
|
|
class SearchEventOperator(str, Enum):
|
|
IS = "is"
|
|
IS_ANY = "isAny"
|
|
ON = "on"
|
|
ON_ANY = "onAny"
|
|
IS_NOT = "isNot"
|
|
IS_UNDEFINED = "isUndefined"
|
|
NOT_ON = "notOn"
|
|
CONTAINS = "contains"
|
|
NOT_CONTAINS = "notContains"
|
|
STARTS_WITH = "startsWith"
|
|
ENDS_WITH = "endsWith"
|
|
|
|
|
|
class ClickEventExtraOperator(str, Enum):
|
|
IS = "selectorIs"
|
|
IS_ANY = "selectorIsAny"
|
|
IS_NOT = "selectorIsNot"
|
|
IS_UNDEFINED = "selectorIsUndefined"
|
|
CONTAINS = "selectorContains"
|
|
NOT_CONTAINS = "selectorNotContains"
|
|
STARTS_WITH = "selectorStartsWith"
|
|
ENDS_WITH = "selectorEndsWith"
|
|
|
|
|
|
class PlatformType(str, Enum):
|
|
MOBILE = "mobile"
|
|
DESKTOP = "desktop"
|
|
TABLET = "tablet"
|
|
|
|
|
|
class SearchEventOrder(str, Enum):
|
|
THEN = "then"
|
|
OR = "or"
|
|
AND = "and"
|
|
|
|
|
|
class IssueType(str, Enum):
|
|
CLICK_RAGE = 'click_rage'
|
|
DEAD_CLICK = 'dead_click'
|
|
EXCESSIVE_SCROLLING = 'excessive_scrolling'
|
|
BAD_REQUEST = 'bad_request'
|
|
MISSING_RESOURCE = 'missing_resource'
|
|
MEMORY = 'memory'
|
|
CPU = 'cpu'
|
|
SLOW_RESOURCE = 'slow_resource'
|
|
SLOW_PAGE_LOAD = 'slow_page_load'
|
|
CRASH = 'crash'
|
|
CUSTOM = 'custom'
|
|
JS_EXCEPTION = 'js_exception'
|
|
MOUSE_THRASHING = 'mouse_thrashing'
|
|
# IOS
|
|
TAP_RAGE = 'tap_rage'
|
|
|
|
|
|
class MetricFormatType(str, Enum):
|
|
SESSION_COUNT = 'sessionCount'
|
|
|
|
|
|
class MetricExtendedFormatType(str, Enum):
|
|
SESSION_COUNT = 'sessionCount'
|
|
USER_COUNT = 'userCount'
|
|
|
|
|
|
class FetchFilterType(str, Enum):
|
|
FETCH_URL = "fetchUrl"
|
|
FETCH_STATUS_CODE = "fetchStatusCode"
|
|
FETCH_METHOD = "fetchMethod"
|
|
FETCH_DURATION = "fetchDuration"
|
|
FETCH_REQUEST_BODY = "fetchRequestBody"
|
|
FETCH_RESPONSE_BODY = "fetchResponseBody"
|
|
|
|
|
|
class GraphqlFilterType(str, Enum):
|
|
GRAPHQL_NAME = "graphqlName"
|
|
GRAPHQL_METHOD = "graphqlMethod"
|
|
GRAPHQL_REQUEST_BODY = "graphqlRequestBody"
|
|
GRAPHQL_RESPONSE_BODY = "graphqlResponseBody"
|
|
|
|
|
|
class RequestGraphqlFilterSchema(BaseModel):
|
|
type: Union[FetchFilterType, GraphqlFilterType] = Field(...)
|
|
value: List[Union[int, str]] = Field(...)
|
|
operator: Union[SearchEventOperator, MathOperator] = Field(...)
|
|
|
|
|
|
class SessionSearchEventSchema2(BaseModel):
|
|
is_event: Literal[True] = True
|
|
value: List[Union[str, int]] = Field(...)
|
|
type: Union[EventType, PerformanceEventType] = Field(...)
|
|
operator: Union[SearchEventOperator, ClickEventExtraOperator] = Field(...)
|
|
source: Optional[List[Union[ErrorSource, int, str]]] = Field(default=None)
|
|
sourceOperator: Optional[MathOperator] = Field(default=None)
|
|
filters: Optional[List[RequestGraphqlFilterSchema]] = Field(default=[])
|
|
|
|
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
|
|
_single_to_list_values = field_validator('value', mode='before')(single_to_list)
|
|
_transform = model_validator(mode='before')(transform_old_filter_type)
|
|
|
|
@model_validator(mode='after')
|
|
def event_validator(cls, values):
|
|
if isinstance(values.type, PerformanceEventType):
|
|
if values.type == PerformanceEventType.FETCH_FAILED:
|
|
return values
|
|
# assert values.get("source") is not None, "source should not be null for PerformanceEventType"
|
|
# assert isinstance(values["source"], list) and len(values["source"]) > 0, \
|
|
# "source should not be empty for PerformanceEventType"
|
|
assert values.sourceOperator is not None, \
|
|
"sourceOperator should not be null for PerformanceEventType"
|
|
assert "source" in values, f"source is required for {values.type}"
|
|
assert isinstance(values.source, list), f"source of type list is required for {values.type}"
|
|
for c in values["source"]:
|
|
assert isinstance(c, int), f"source value should be of type int for {values.type}"
|
|
elif values.type == EventType.ERROR and values.source is None:
|
|
values.source = [ErrorSource.JS_EXCEPTION]
|
|
elif values.type == EventType.REQUEST_DETAILS:
|
|
assert isinstance(values.filters, List) and len(values.filters) > 0, \
|
|
f"filters should be defined for {EventType.REQUEST_DETAILS}"
|
|
elif values.type == EventType.GRAPHQL:
|
|
assert isinstance(values.filters, List) and len(values.filters) > 0, \
|
|
f"filters should be defined for {EventType.GRAPHQL}"
|
|
|
|
if isinstance(values.operator, ClickEventExtraOperator):
|
|
assert values.type == EventType.CLICK, \
|
|
f"operator:{values.operator} is only available for event-type: {EventType.CLICK}"
|
|
return values
|
|
|
|
|
|
class SessionSearchFilterSchema(BaseModel):
|
|
is_event: Literal[False] = False
|
|
value: List[Union[IssueType, PlatformType, int, str]] = Field(default=[])
|
|
type: FilterType = Field(...)
|
|
operator: Union[SearchEventOperator, MathOperator] = Field(...)
|
|
source: Optional[Union[ErrorSource, str]] = Field(default=None)
|
|
|
|
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
|
|
_transform = model_validator(mode='before')(transform_old_filter_type)
|
|
_single_to_list_values = field_validator('value', mode='before')(single_to_list)
|
|
|
|
@model_validator(mode='before')
|
|
def _transform_data(cls, values):
|
|
if values.get("source") is not None:
|
|
if isinstance(values["source"], list):
|
|
if len(values["source"]) == 0:
|
|
values["source"] = None
|
|
elif len(values["source"]) == 1:
|
|
values["source"] = values["source"][0]
|
|
else:
|
|
raise ValueError(f"Unsupported multi-values source")
|
|
return values
|
|
|
|
@model_validator(mode='after')
|
|
def filter_validator(cls, values):
|
|
if values.type == FilterType.METADATA:
|
|
assert values.source is not None and len(values.source) > 0, \
|
|
"must specify a valid 'source' for metadata filter"
|
|
elif values.type == FilterType.ISSUE:
|
|
for v in values.value:
|
|
if IssueType.has_value(v):
|
|
v = IssueType(v)
|
|
else:
|
|
raise ValueError(f"value should be of type IssueType for {values.type} filter")
|
|
elif values.type == FilterType.PLATFORM:
|
|
for v in values.value:
|
|
if PlatformType.has_value(v):
|
|
v = PlatformType(v)
|
|
else:
|
|
raise ValueError(f"value should be of type PlatformType for {values.type} filter")
|
|
elif values.type == FilterType.EVENTS_COUNT:
|
|
if MathOperator.has_value(values.operator):
|
|
values.operator = MathOperator(values.operator)
|
|
else:
|
|
raise ValueError(f"operator should be of type MathOperator for {values.type} filter")
|
|
|
|
for v in values.value:
|
|
assert isinstance(v, int), f"value should be of type int for {values.type} filter"
|
|
else:
|
|
if SearchEventOperator.has_value(values.operator):
|
|
values.operator = SearchEventOperator(values.operator)
|
|
else:
|
|
raise ValueError(f"operator should be of type SearchEventOperator for {values.type} filter")
|
|
|
|
return values
|
|
|
|
|
|
class _PaginatedSchema(BaseModel):
|
|
limit: int = Field(default=200, gt=0, le=200)
|
|
page: int = Field(default=1, gt=0)
|
|
|
|
|
|
class SortOrderType(str, Enum):
|
|
ASC = "ASC"
|
|
DESC = "DESC"
|
|
|
|
|
|
def add_missing_is_event(values: dict):
|
|
if values.get("isEvent") is None:
|
|
values["isEvent"] = (EventType.has_value(values["type"])
|
|
or PerformanceEventType.has_value(values["type"])
|
|
or ProductAnalyticsSelectedEventType.has_value(values["type"]))
|
|
return values
|
|
|
|
|
|
# this type is created to allow mixing events&filters and specifying a discriminator
|
|
GroupedFilterType = Annotated[Union[SessionSearchFilterSchema, SessionSearchEventSchema2], \
|
|
Field(discriminator='is_event'), BeforeValidator(add_missing_is_event)]
|
|
|
|
|
|
class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
|
|
events: List[SessionSearchEventSchema2] = Field(default=[], doc_hidden=True)
|
|
filters: List[GroupedFilterType] = Field(default=[])
|
|
sort: str = Field(default="startTs")
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
events_order: Optional[SearchEventOrder] = Field(default=SearchEventOrder.THEN)
|
|
group_by_user: bool = Field(default=False)
|
|
bookmarked: bool = Field(default=False)
|
|
|
|
@model_validator(mode="before")
|
|
def transform_order(cls, values):
|
|
if values.get("sort") is None:
|
|
values["sort"] = "startTs"
|
|
|
|
if values.get("order") is None:
|
|
values["order"] = SortOrderType.DESC
|
|
else:
|
|
values["order"] = values["order"].upper()
|
|
return values
|
|
|
|
@model_validator(mode="before")
|
|
def add_missing_attributes(cls, values):
|
|
# in case isEvent is wrong:
|
|
for f in values.get("filters") or []:
|
|
if EventType.has_value(f["type"]) and not f.get("isEvent"):
|
|
f["isEvent"] = True
|
|
elif FilterType.has_value(f["type"]) and f.get("isEvent"):
|
|
f["isEvent"] = False
|
|
|
|
# in case the old search payload was passed
|
|
for v in values.get("events") or []:
|
|
v["isEvent"] = True
|
|
|
|
return values
|
|
|
|
@model_validator(mode="before")
|
|
def remove_wrong_filter_values(cls, values):
|
|
for f in values.get("filters", []):
|
|
vals = []
|
|
for v in f.get("value", []):
|
|
if v is not None:
|
|
vals.append(v)
|
|
f["value"] = vals
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def split_filters_events(cls, values):
|
|
n_filters = []
|
|
n_events = []
|
|
for v in values.filters:
|
|
if v.is_event:
|
|
n_events.append(v)
|
|
else:
|
|
n_filters.append(v)
|
|
values.events = n_events
|
|
values.filters = n_filters
|
|
return values
|
|
|
|
@field_validator("filters", mode="after")
|
|
def merge_identical_filters(cls, values):
|
|
# ignore 'issue' type as it could be used for step-filters and tab-filters at the same time
|
|
i = 0
|
|
while i < len(values):
|
|
if values[i].is_event or values[i].type == FilterType.ISSUE:
|
|
if values[i].type == FilterType.ISSUE:
|
|
values[i] = remove_duplicate_values(values[i])
|
|
i += 1
|
|
continue
|
|
j = i + 1
|
|
while j < len(values):
|
|
if values[i].type == values[j].type \
|
|
and values[i].operator == values[j].operator \
|
|
and (values[i].type != FilterType.METADATA or values[i].source == values[j].source):
|
|
values[i].value += values[j].value
|
|
del values[j]
|
|
else:
|
|
j += 1
|
|
values[i] = remove_duplicate_values(values[i])
|
|
i += 1
|
|
|
|
return values
|
|
|
|
|
|
class ErrorStatus(str, Enum):
|
|
ALL = 'all'
|
|
UNRESOLVED = 'unresolved'
|
|
RESOLVED = 'resolved'
|
|
IGNORED = 'ignored'
|
|
|
|
|
|
class ErrorSort(str, Enum):
|
|
OCCURRENCE = 'occurrence'
|
|
USERS_COUNT = 'users'
|
|
SESSIONS_COUNT = 'sessions'
|
|
|
|
|
|
class SearchErrorsSchema(SessionsSearchPayloadSchema):
|
|
sort: ErrorSort = Field(default=ErrorSort.OCCURRENCE)
|
|
density: Optional[int] = Field(default=7)
|
|
status: Optional[ErrorStatus] = Field(default=ErrorStatus.ALL)
|
|
query: Optional[str] = Field(default=None)
|
|
|
|
|
|
class ProductAnalyticsSelectedEventType(str, Enum):
|
|
CLICK = EventType.CLICK.value
|
|
INPUT = EventType.INPUT.value
|
|
LOCATION = EventType.LOCATION.value
|
|
CUSTOM_EVENT = EventType.CUSTOM.value
|
|
|
|
|
|
class PathAnalysisSubFilterSchema(BaseModel):
|
|
is_event: Literal[True] = True
|
|
value: List[str] = Field(...)
|
|
type: ProductAnalyticsSelectedEventType = Field(...)
|
|
operator: Union[SearchEventOperator, ClickEventExtraOperator] = Field(...)
|
|
|
|
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
|
|
|
|
@model_validator(mode="before")
|
|
def __force_is_event(cls, values):
|
|
values["isEvent"] = True
|
|
return values
|
|
|
|
|
|
class _ProductAnalyticsFilter(BaseModel):
|
|
is_event: Literal[False] = False
|
|
type: FilterType
|
|
operator: Union[SearchEventOperator, ClickEventExtraOperator, MathOperator] = Field(...)
|
|
value: List[Union[IssueType, PlatformType, int, str]] = Field(...)
|
|
source: Optional[str] = Field(default=None)
|
|
|
|
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
|
|
|
|
|
|
class _ProductAnalyticsEventFilter(BaseModel):
|
|
is_event: Literal[True] = True
|
|
type: ProductAnalyticsSelectedEventType
|
|
operator: Union[SearchEventOperator, ClickEventExtraOperator, MathOperator] = Field(...)
|
|
# TODO: support session metadata filters
|
|
value: List[Union[IssueType, PlatformType, int, str]] = Field(...)
|
|
|
|
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
|
|
|
|
|
|
# this type is created to allow mixing events&filters and specifying a discriminator for PathAnalysis series filter
|
|
ProductAnalyticsFilter = Annotated[Union[_ProductAnalyticsFilter, _ProductAnalyticsEventFilter], \
|
|
Field(discriminator='is_event')]
|
|
|
|
|
|
class PathAnalysisSchema(_TimedSchema, _PaginatedSchema):
|
|
density: int = Field(default=7)
|
|
filters: List[ProductAnalyticsFilter] = Field(default=[])
|
|
type: Optional[str] = Field(default=None)
|
|
|
|
_transform_filters = field_validator('filters', mode='before') \
|
|
(force_is_event(events_enum=[ProductAnalyticsSelectedEventType]))
|
|
|
|
|
|
class MobileSignPayloadSchema(BaseModel):
|
|
keys: List[str] = Field(...)
|
|
|
|
|
|
class CardSeriesFilterSchema(SearchErrorsSchema):
|
|
sort: Optional[str] = Field(default=None)
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
group_by_user: Literal[False] = False
|
|
|
|
|
|
class CardSeriesSchema(BaseModel):
|
|
series_id: Optional[int] = Field(default=None)
|
|
name: Optional[str] = Field(default=None)
|
|
index: Optional[int] = Field(default=None)
|
|
filter: Optional[CardSeriesFilterSchema] = Field(default=None)
|
|
|
|
|
|
class MetricTimeseriesViewType(str, Enum):
|
|
LINE_CHART = "lineChart"
|
|
AREA_CHART = "areaChart"
|
|
|
|
|
|
class MetricTableViewType(str, Enum):
|
|
TABLE = "table"
|
|
|
|
|
|
class MetricOtherViewType(str, Enum):
|
|
OTHER_CHART = "chart"
|
|
LIST_CHART = "list"
|
|
|
|
|
|
class MetricType(str, Enum):
|
|
TIMESERIES = "timeseries"
|
|
TABLE = "table"
|
|
FUNNEL = "funnel"
|
|
ERRORS = "errors"
|
|
PERFORMANCE = "performance"
|
|
RESOURCES = "resources"
|
|
WEB_VITAL = "webVitals"
|
|
PATH_ANALYSIS = "pathAnalysis"
|
|
RETENTION = "retention"
|
|
STICKINESS = "stickiness"
|
|
HEAT_MAP = "heatMap"
|
|
INSIGHTS = "insights"
|
|
|
|
|
|
class MetricOfErrors(str, Enum):
|
|
CALLS_ERRORS = "callsErrors"
|
|
DOMAINS_ERRORS_4XX = "domainsErrors4xx"
|
|
DOMAINS_ERRORS_5XX = "domainsErrors5xx"
|
|
ERRORS_PER_DOMAINS = "errorsPerDomains"
|
|
ERRORS_PER_TYPE = "errorsPerType"
|
|
IMPACTED_SESSIONS_BY_JS_ERRORS = "impactedSessionsByJsErrors"
|
|
RESOURCES_BY_PARTY = "resourcesByParty"
|
|
|
|
|
|
class MetricOfPerformance(str, Enum):
|
|
CPU = "cpu"
|
|
CRASHES = "crashes"
|
|
FPS = "fps"
|
|
IMPACTED_SESSIONS_BY_SLOW_PAGES = "impactedSessionsBySlowPages"
|
|
MEMORY_CONSUMPTION = "memoryConsumption"
|
|
PAGES_DOM_BUILDTIME = "pagesDomBuildtime"
|
|
PAGES_RESPONSE_TIME = "pagesResponseTime"
|
|
PAGES_RESPONSE_TIME_DISTRIBUTION = "pagesResponseTimeDistribution"
|
|
RESOURCES_VS_VISUALLY_COMPLETE = "resourcesVsVisuallyComplete"
|
|
SESSIONS_PER_BROWSER = "sessionsPerBrowser"
|
|
SLOWEST_DOMAINS = "slowestDomains"
|
|
SPEED_LOCATION = "speedLocation"
|
|
TIME_TO_RENDER = "timeToRender"
|
|
|
|
|
|
class MetricOfResources(str, Enum):
|
|
MISSING_RESOURCES = "missingResources"
|
|
RESOURCES_COUNT_BY_TYPE = "resourcesCountByType"
|
|
RESOURCES_LOADING_TIME = "resourcesLoadingTime"
|
|
RESOURCE_TYPE_VS_RESPONSE_END = "resourceTypeVsResponseEnd"
|
|
SLOWEST_RESOURCES = "slowestResources"
|
|
|
|
|
|
class MetricOfWebVitals(str, Enum):
|
|
AVG_CPU = "avgCpu"
|
|
AVG_DOM_CONTENT_LOADED = "avgDomContentLoaded"
|
|
AVG_DOM_CONTENT_LOAD_START = "avgDomContentLoadStart"
|
|
AVG_FIRST_CONTENTFUL_PIXEL = "avgFirstContentfulPixel"
|
|
AVG_FIRST_PAINT = "avgFirstPaint"
|
|
AVG_FPS = "avgFps"
|
|
AVG_IMAGE_LOAD_TIME = "avgImageLoadTime"
|
|
AVG_PAGE_LOAD_TIME = "avgPageLoadTime"
|
|
AVG_PAGES_DOM_BUILDTIME = "avgPagesDomBuildtime"
|
|
AVG_PAGES_RESPONSE_TIME = "avgPagesResponseTime"
|
|
AVG_REQUEST_LOAD_TIME = "avgRequestLoadTime"
|
|
AVG_RESPONSE_TIME = "avgResponseTime"
|
|
AVG_SESSION_DURATION = "avgSessionDuration"
|
|
AVG_TILL_FIRST_BYTE = "avgTillFirstByte"
|
|
AVG_TIME_TO_INTERACTIVE = "avgTimeToInteractive"
|
|
AVG_TIME_TO_RENDER = "avgTimeToRender"
|
|
AVG_USED_JS_HEAP_SIZE = "avgUsedJsHeapSize"
|
|
AVG_VISITED_PAGES = "avgVisitedPages"
|
|
COUNT_REQUESTS = "countRequests"
|
|
COUNT_SESSIONS = "countSessions"
|
|
COUNT_USERS = "countUsers"
|
|
|
|
|
|
class MetricOfTable(str, Enum):
|
|
USER_OS = FilterType.USER_OS.value
|
|
USER_BROWSER = FilterType.USER_BROWSER.value
|
|
USER_DEVICE = FilterType.USER_DEVICE.value
|
|
USER_COUNTRY = FilterType.USER_COUNTRY.value
|
|
# user_city = FilterType.user_city.value
|
|
# user_state = FilterType.user_state.value
|
|
USER_ID = FilterType.USER_ID.value
|
|
ISSUES = FilterType.ISSUE.value
|
|
VISITED_URL = "location"
|
|
SESSIONS = "sessions"
|
|
ERRORS = "jsException"
|
|
|
|
|
|
class MetricOfTimeseries(str, Enum):
|
|
SESSION_COUNT = "sessionCount"
|
|
USER_COUNT = "userCount"
|
|
|
|
|
|
class MetricOfFunnels(str, Enum):
|
|
SESSION_COUNT = MetricOfTimeseries.SESSION_COUNT.value
|
|
USER_COUNT = MetricOfTimeseries.USER_COUNT.value
|
|
|
|
|
|
class MetricOfHeatMap(str, Enum):
|
|
HEAT_MAP_URL = "heatMapUrl"
|
|
|
|
|
|
class MetricOfPathAnalysis(str, Enum):
|
|
session_count = MetricOfTimeseries.SESSION_COUNT.value
|
|
|
|
|
|
# class CardSessionsSchema(SessionsSearchPayloadSchema):
|
|
class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
|
startTimestamp: int = Field(default=TimeUTC.now(-7))
|
|
endTimestamp: int = Field(defautl=TimeUTC.now())
|
|
density: int = Field(default=7, ge=1, le=200)
|
|
series: List[CardSeriesSchema] = Field(default=[])
|
|
|
|
# events: List[SessionSearchEventSchema2] = Field(default=[], doc_hidden=True)
|
|
filters: List[GroupedFilterType] = Field(default=[])
|
|
|
|
# Used mainly for PathAnalysis, and could be used by other cards
|
|
hide_excess: Optional[bool] = Field(default=False, description="Hide extra values")
|
|
|
|
_transform_filters = field_validator('filters', mode='before') \
|
|
(force_is_event(events_enum=[EventType, PerformanceEventType]))
|
|
|
|
@model_validator(mode="before")
|
|
def remove_wrong_filter_values(cls, values):
|
|
for f in values.get("filters", []):
|
|
vals = []
|
|
for v in f.get("value", []):
|
|
if v is not None:
|
|
vals.append(v)
|
|
f["value"] = vals
|
|
return values
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
if values.get("startTimestamp") is None:
|
|
values["startTimestamp"] = TimeUTC.now(-7)
|
|
|
|
if values.get("endTimestamp") is None:
|
|
values["endTimestamp"] = TimeUTC.now()
|
|
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __enforce_default_after(cls, values):
|
|
for s in values.series:
|
|
if s.filter is not None:
|
|
s.filter.limit = values.limit
|
|
s.filter.page = values.page
|
|
s.filter.startTimestamp = values.startTimestamp
|
|
s.filter.endTimestamp = values.endTimestamp
|
|
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __merge_out_filters_with_series(cls, values):
|
|
if len(values.filters) > 0:
|
|
for f in values.filters:
|
|
for s in values.series:
|
|
found = False
|
|
|
|
if f.is_event:
|
|
sub = s.filter.events
|
|
else:
|
|
sub = s.filter.filters
|
|
|
|
for e in sub:
|
|
if f.type == e.type and f.operator == e.operator:
|
|
found = True
|
|
if f.is_event:
|
|
# If extra event: append value
|
|
for v in f.value:
|
|
if v not in e.value:
|
|
e.value.append(v)
|
|
else:
|
|
# If extra filter: override value
|
|
e.value = f.value
|
|
if not found:
|
|
sub.append(f)
|
|
|
|
values.filters = []
|
|
|
|
return values
|
|
|
|
|
|
class CardConfigSchema(BaseModel):
|
|
col: Optional[int] = Field(default=None)
|
|
row: Optional[int] = Field(default=2)
|
|
position: Optional[int] = Field(default=0)
|
|
|
|
|
|
class __CardSchema(CardSessionsSchema):
|
|
name: Optional[str] = Field(default=None)
|
|
is_public: bool = Field(default=True)
|
|
default_config: CardConfigSchema = Field(default=CardConfigSchema(), alias="config")
|
|
thumbnail: Optional[str] = Field(default=None)
|
|
metric_format: Optional[MetricFormatType] = Field(default=None)
|
|
view_type: Any
|
|
metric_type: MetricType = Field(...)
|
|
metric_of: Any
|
|
metric_value: List[IssueType] = Field(default=[])
|
|
# This is used to save the selected session for heatmaps
|
|
session_id: Optional[int] = Field(default=None)
|
|
|
|
@computed_field
|
|
@property
|
|
def is_predefined(self) -> bool:
|
|
return self.metric_type in [MetricType.ERRORS, MetricType.PERFORMANCE,
|
|
MetricType.RESOURCES, MetricType.WEB_VITAL]
|
|
|
|
|
|
class CardTimeSeries(__CardSchema):
|
|
metric_type: Literal[MetricType.TIMESERIES]
|
|
metric_of: MetricOfTimeseries = Field(default=MetricOfTimeseries.SESSION_COUNT)
|
|
view_type: MetricTimeseriesViewType
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["metricValue"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfTimeseries(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardTable(__CardSchema):
|
|
metric_type: Literal[MetricType.TABLE]
|
|
metric_of: MetricOfTable = Field(default=MetricOfTable.USER_ID)
|
|
view_type: MetricTableViewType = Field(...)
|
|
metric_format: MetricExtendedFormatType = Field(default=MetricExtendedFormatType.SESSION_COUNT)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
if values.get("metricOf") is not None and values.get("metricOf") != MetricOfTable.ISSUES:
|
|
values["metricValue"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfTable(values.metric_of)
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __validator(cls, values):
|
|
if values.metric_of not in (MetricOfTable.ISSUES, MetricOfTable.USER_BROWSER,
|
|
MetricOfTable.USER_DEVICE, MetricOfTable.USER_COUNTRY,
|
|
MetricOfTable.VISITED_URL):
|
|
assert values.metric_format == MetricExtendedFormatType.SESSION_COUNT, \
|
|
f'metricFormat:{MetricExtendedFormatType.USER_COUNT.value} is not supported for this metricOf'
|
|
return values
|
|
|
|
|
|
class CardFunnel(__CardSchema):
|
|
metric_type: Literal[MetricType.FUNNEL]
|
|
metric_of: MetricOfFunnels = Field(default=MetricOfFunnels.SESSION_COUNT)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
if values.get("metricOf") and not MetricOfFunnels.has_value(values["metricOf"]):
|
|
values["metricOf"] = MetricOfFunnels.SESSION_COUNT
|
|
values["viewType"] = MetricOtherViewType.OTHER_CHART
|
|
if values.get("series") is not None and len(values["series"]) > 0:
|
|
values["series"] = [values["series"][0]]
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfTimeseries(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardErrors(__CardSchema):
|
|
metric_type: Literal[MetricType.ERRORS]
|
|
metric_of: MetricOfErrors = Field(default=MetricOfErrors.IMPACTED_SESSIONS_BY_JS_ERRORS)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["series"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfErrors(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardPerformance(__CardSchema):
|
|
metric_type: Literal[MetricType.PERFORMANCE]
|
|
metric_of: MetricOfPerformance = Field(default=MetricOfPerformance.CPU)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["series"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfPerformance(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardResources(__CardSchema):
|
|
metric_type: Literal[MetricType.RESOURCES]
|
|
metric_of: MetricOfResources = Field(default=MetricOfResources.MISSING_RESOURCES)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["series"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfResources(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardWebVital(__CardSchema):
|
|
metric_type: Literal[MetricType.WEB_VITAL]
|
|
metric_of: MetricOfWebVitals = Field(default=MetricOfWebVitals.AVG_CPU)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["series"] = []
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfWebVitals(values.metric_of)
|
|
return values
|
|
|
|
|
|
class CardHeatMap(__CardSchema):
|
|
metric_type: Literal[MetricType.HEAT_MAP]
|
|
metric_of: MetricOfHeatMap = Field(default=MetricOfHeatMap.HEAT_MAP_URL)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfHeatMap(values.metric_of)
|
|
return values
|
|
|
|
|
|
class MetricOfInsights(str, Enum):
|
|
ISSUE_CATEGORIES = "issueCategories"
|
|
|
|
|
|
class CardInsights(__CardSchema):
|
|
metric_type: Literal[MetricType.INSIGHTS]
|
|
metric_of: MetricOfInsights = Field(default=MetricOfInsights.ISSUE_CATEGORIES)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["view_type"] = MetricOtherViewType.LIST_CHART
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __transform(cls, values):
|
|
values.metric_of = MetricOfInsights(values.metric_of)
|
|
return values
|
|
|
|
@model_validator(mode='after')
|
|
def restrictions(cls, values):
|
|
raise ValueError(f"metricType:{MetricType.INSIGHTS} not supported yet.")
|
|
|
|
|
|
class CardPathAnalysisSeriesSchema(CardSeriesSchema):
|
|
name: Optional[str] = Field(default=None)
|
|
filter: PathAnalysisSchema = Field(...)
|
|
density: int = Field(default=4, ge=2, le=10)
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
if values.get("filter") is None and values.get("startTimestamp") and values.get("endTimestamp"):
|
|
values["filter"] = PathAnalysisSchema(startTimestamp=values["startTimestamp"],
|
|
endTimestamp=values["endTimestamp"],
|
|
density=values.get("density", 4))
|
|
return values
|
|
|
|
|
|
class CardPathAnalysis(__CardSchema):
|
|
metric_type: Literal[MetricType.PATH_ANALYSIS]
|
|
metric_of: MetricOfPathAnalysis = Field(default=MetricOfPathAnalysis.session_count)
|
|
view_type: MetricOtherViewType = Field(...)
|
|
metric_value: List[ProductAnalyticsSelectedEventType] = Field(default=[])
|
|
density: int = Field(default=4, ge=2, le=10)
|
|
|
|
start_type: Literal["start", "end"] = Field(default="start")
|
|
start_point: List[PathAnalysisSubFilterSchema] = Field(default=[])
|
|
excludes: List[PathAnalysisSubFilterSchema] = Field(default=[])
|
|
|
|
series: List[CardPathAnalysisSeriesSchema] = Field(default=[])
|
|
|
|
@model_validator(mode="before")
|
|
def __enforce_default(cls, values):
|
|
values["viewType"] = MetricOtherViewType.OTHER_CHART.value
|
|
if values.get("series") is not None and len(values["series"]) > 0:
|
|
values["series"] = [values["series"][0]]
|
|
return values
|
|
|
|
@model_validator(mode="after")
|
|
def __clean_start_point_and_enforce_metric_value(cls, values):
|
|
start_point = []
|
|
for s in values.start_point:
|
|
if len(s.value) == 0:
|
|
continue
|
|
start_point.append(s)
|
|
values.metric_value.append(s.type)
|
|
|
|
values.start_point = start_point
|
|
values.metric_value = remove_duplicate_values(values.metric_value)
|
|
|
|
return values
|
|
|
|
@model_validator(mode='after')
|
|
def __validator(cls, values):
|
|
s_e_values = {}
|
|
exclude_values = {}
|
|
for f in values.start_point:
|
|
s_e_values[f.type] = s_e_values.get(f.type, []) + f.value
|
|
|
|
for f in values.excludes:
|
|
exclude_values[f.type] = exclude_values.get(f.type, []) + f.value
|
|
|
|
assert len(
|
|
values.start_point) <= 1, f"Only 1 startPoint with multiple values OR 1 endPoint with multiple values is allowed"
|
|
for t in exclude_values:
|
|
for v in t:
|
|
assert v not in s_e_values.get(t, []), f"startPoint and endPoint cannot be excluded, value: {v}"
|
|
|
|
return values
|
|
|
|
|
|
# Union of cards-schemas that doesn't change between FOSS and EE
|
|
__cards_union_base = Union[
|
|
CardTimeSeries, CardTable, CardFunnel,
|
|
CardErrors, CardPerformance, CardResources,
|
|
CardWebVital, CardHeatMap,
|
|
CardPathAnalysis]
|
|
CardSchema = ORUnion(Union[__cards_union_base, CardInsights], discriminator='metric_type')
|
|
|
|
|
|
class UpdateCardStatusSchema(BaseModel):
|
|
active: bool = Field(...)
|
|
|
|
|
|
class SavedSearchSchema(BaseModel):
|
|
name: str = Field(...)
|
|
is_public: bool = Field(default=False)
|
|
filter: SessionsSearchPayloadSchema = Field([])
|
|
|
|
|
|
class ProjectConditions(BaseModel):
|
|
condition_id: Optional[int] = Field(default=None)
|
|
name: str = Field(...)
|
|
capture_rate: int = Field(..., ge=0, le=100)
|
|
filters: List[GroupedFilterType] = Field(default=[])
|
|
|
|
|
|
class ProjectSettings(BaseModel):
|
|
rate: int = Field(..., ge=0, le=100)
|
|
conditional_capture: bool = Field(default=False)
|
|
conditions: List[ProjectConditions] = Field(default=[])
|
|
|
|
|
|
class CreateDashboardSchema(BaseModel):
|
|
name: str = Field(..., min_length=1)
|
|
description: Optional[str] = Field(default='')
|
|
is_public: bool = Field(default=False)
|
|
is_pinned: bool = Field(default=False)
|
|
metrics: Optional[List[int]] = Field(default=[])
|
|
|
|
|
|
class EditDashboardSchema(CreateDashboardSchema):
|
|
is_public: Optional[bool] = Field(default=None)
|
|
is_pinned: Optional[bool] = Field(default=None)
|
|
|
|
|
|
class UpdateWidgetPayloadSchema(BaseModel):
|
|
config: dict = Field(default={})
|
|
|
|
|
|
class AddWidgetToDashboardPayloadSchema(UpdateWidgetPayloadSchema):
|
|
metric_id: int = Field(...)
|
|
|
|
|
|
class TemplatePredefinedUnits(str, Enum):
|
|
MILLISECOND = "ms"
|
|
SECOND = "s"
|
|
MINUTE = "min"
|
|
MEMORY = "mb"
|
|
FRAME = "f/s"
|
|
PERCENTAGE = "%"
|
|
COUNT = "count"
|
|
|
|
|
|
class LiveFilterType(str, Enum):
|
|
USER_OS = FilterType.USER_OS.value
|
|
USER_BROWSER = FilterType.USER_BROWSER.value
|
|
USER_DEVICE = FilterType.USER_DEVICE.value
|
|
USER_COUNTRY = FilterType.USER_COUNTRY.value
|
|
USER_ID = FilterType.USER_ID.value
|
|
USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value
|
|
REV_ID = FilterType.REV_ID.value
|
|
PLATFORM = FilterType.PLATFORM.value
|
|
PAGE_TITLE = "pageTitle"
|
|
SESSION_ID = "sessionId"
|
|
METADATA = FilterType.METADATA.value
|
|
USER_UUID = "userUuid"
|
|
TRACKER_VERSION = "trackerVersion"
|
|
USER_BROWSER_VERSION = "userBrowserVersion"
|
|
USER_DEVICE_TYPE = "userDeviceType"
|
|
|
|
|
|
class LiveSessionSearchFilterSchema(BaseModel):
|
|
value: Union[List[str], str] = Field(...)
|
|
type: LiveFilterType = Field(...)
|
|
source: Optional[str] = Field(default=None)
|
|
operator: Literal[SearchEventOperator.IS, \
|
|
SearchEventOperator.CONTAINS] = Field(default=SearchEventOperator.CONTAINS)
|
|
|
|
_transform = model_validator(mode='before')(transform_old_filter_type)
|
|
|
|
@model_validator(mode='after')
|
|
def __validator(cls, values):
|
|
if values.type is not None and values.type == LiveFilterType.METADATA:
|
|
assert values.source is not None, "source should not be null for METADATA type"
|
|
assert len(values.source) > 0, "source should not be empty for METADATA type"
|
|
return values
|
|
|
|
|
|
class LiveSessionsSearchPayloadSchema(_PaginatedSchema):
|
|
filters: List[LiveSessionSearchFilterSchema] = Field([])
|
|
sort: Union[LiveFilterType, str] = Field(default="TIMESTAMP")
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
|
|
@model_validator(mode="before")
|
|
def __transform(cls, values):
|
|
if values.get("order") is not None:
|
|
values["order"] = values["order"].upper()
|
|
if values.get("filters") is not None:
|
|
i = 0
|
|
while i < len(values["filters"]):
|
|
if values["filters"][i]["value"] is None or len(values["filters"][i]["value"]) == 0:
|
|
del values["filters"][i]
|
|
else:
|
|
i += 1
|
|
for i in values["filters"]:
|
|
if i.get("type") == LiveFilterType.PLATFORM:
|
|
i["type"] = LiveFilterType.USER_DEVICE_TYPE
|
|
if values.get("sort") is not None:
|
|
if values["sort"].lower() == "startts":
|
|
values["sort"] = "TIMESTAMP"
|
|
return values
|
|
|
|
|
|
class IntegrationType(str, Enum):
|
|
GITHUB = "GITHUB"
|
|
JIRA = "JIRA"
|
|
SLACK = "SLACK"
|
|
MS_TEAMS = "MSTEAMS"
|
|
SENTRY = "SENTRY"
|
|
BUGSNAG = "BUGSNAG"
|
|
ROLLBAR = "ROLLBAR"
|
|
ELASTICSEARCH = "ELASTICSEARCH"
|
|
DATADOG = "DATADOG"
|
|
SUMOLOGIC = "SUMOLOGIC"
|
|
STACKDRIVER = "STACKDRIVER"
|
|
CLOUDWATCH = "CLOUDWATCH"
|
|
NEWRELIC = "NEWRELIC"
|
|
|
|
|
|
class SearchNoteSchema(_PaginatedSchema):
|
|
sort: str = Field(default="createdAt")
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
tags: Optional[List[str]] = Field(default=[])
|
|
shared_only: bool = Field(default=False)
|
|
mine_only: bool = Field(default=False)
|
|
|
|
|
|
class SessionNoteSchema(BaseModel):
|
|
message: str = Field(..., min_length=2)
|
|
tag: Optional[str] = Field(default=None)
|
|
timestamp: int = Field(default=-1)
|
|
is_public: bool = Field(default=False)
|
|
|
|
|
|
class SessionUpdateNoteSchema(SessionNoteSchema):
|
|
message: Optional[str] = Field(default=None, min_length=2)
|
|
timestamp: Optional[int] = Field(default=None, ge=-1)
|
|
is_public: Optional[bool] = Field(default=None)
|
|
|
|
@model_validator(mode='after')
|
|
def __validator(cls, values):
|
|
assert values.message is not None or values.timestamp is not None or values.is_public is not None, "at least 1 attribute should be provided for update"
|
|
return values
|
|
|
|
|
|
class WebhookType(str, Enum):
|
|
WEBHOOK = "webhook"
|
|
SLACK = "slack"
|
|
EMAIL = "email"
|
|
MSTEAMS = "msteams"
|
|
|
|
|
|
class SearchCardsSchema(_PaginatedSchema):
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
shared_only: bool = Field(default=False)
|
|
mine_only: bool = Field(default=False)
|
|
query: Optional[str] = Field(default=None)
|
|
|
|
|
|
class _HeatMapSearchEventRaw(SessionSearchEventSchema2):
|
|
type: Literal[EventType.LOCATION] = Field(...)
|
|
|
|
|
|
class HeatMapSessionsSearch(SessionsSearchPayloadSchema):
|
|
events: Optional[List[_HeatMapSearchEventRaw]] = Field(default=[])
|
|
filters: List[Union[SessionSearchFilterSchema, _HeatMapSearchEventRaw]] = Field(default=[])
|
|
|
|
@model_validator(mode="before")
|
|
def __transform(cls, values):
|
|
for f in values.get("filters", []):
|
|
if f.get("type") == FilterType.DURATION:
|
|
return values
|
|
values["filters"] = values.get("filters", [])
|
|
values["filters"].append({"value": [5000], "type": FilterType.DURATION,
|
|
"operator": SearchEventOperator.IS, "filters": []})
|
|
return values
|
|
|
|
|
|
class HeatMapFilterSchema(BaseModel):
|
|
value: List[Literal[IssueType.CLICK_RAGE, IssueType.DEAD_CLICK]] = Field(default=[])
|
|
type: Literal[FilterType.ISSUE] = Field(...)
|
|
operator: Literal[SearchEventOperator.IS, MathOperator.EQUAL] = Field(...)
|
|
|
|
|
|
class GetHeatMapPayloadSchema(_TimedSchema):
|
|
url: str = Field(...)
|
|
filters: List[HeatMapFilterSchema] = Field(default=[])
|
|
click_rage: bool = Field(default=False)
|
|
|
|
|
|
class GetClickMapPayloadSchema(GetHeatMapPayloadSchema):
|
|
pass
|
|
|
|
|
|
class FeatureFlagVariant(BaseModel):
|
|
variant_id: Optional[int] = Field(default=None)
|
|
value: str = Field(...)
|
|
description: Optional[str] = Field(default=None)
|
|
payload: Optional[str] = Field(default=None)
|
|
rollout_percentage: Optional[int] = Field(default=0, ge=0, le=100)
|
|
|
|
|
|
class FeatureFlagConditionFilterSchema(BaseModel):
|
|
is_event: Literal[False] = False
|
|
type: FilterType = Field(...)
|
|
value: List[str] = Field(default=[], min_length=1)
|
|
operator: Union[SearchEventOperator, MathOperator] = Field(...)
|
|
source: Optional[str] = Field(default=None)
|
|
sourceOperator: Optional[Union[SearchEventOperator, MathOperator]] = Field(default=None)
|
|
|
|
@model_validator(mode="before")
|
|
def __force_is_event(cls, values):
|
|
values["isEvent"] = False
|
|
return values
|
|
|
|
|
|
class FeatureFlagCondition(BaseModel):
|
|
condition_id: Optional[int] = Field(default=None)
|
|
name: str = Field(...)
|
|
rollout_percentage: Optional[int] = Field(default=0)
|
|
filters: List[FeatureFlagConditionFilterSchema] = Field(default=[])
|
|
|
|
|
|
class SearchFlagsSchema(_PaginatedSchema):
|
|
limit: int = Field(default=15, gt=0, le=200)
|
|
user_id: Optional[int] = Field(default=None)
|
|
order: SortOrderType = Field(default=SortOrderType.DESC)
|
|
query: Optional[str] = Field(default=None)
|
|
is_active: Optional[bool] = Field(default=None)
|
|
|
|
|
|
class FeatureFlagType(str, Enum):
|
|
SINGLE_VARIANT = "single"
|
|
MULTI_VARIANT = "multi"
|
|
|
|
|
|
class FeatureFlagStatus(BaseModel):
|
|
is_active: bool = Field(...)
|
|
|
|
|
|
class FeatureFlagSchema(BaseModel):
|
|
payload: Optional[str] = Field(default=None)
|
|
flag_key: str = Field(..., pattern=r'^[a-zA-Z0-9\-]+$')
|
|
description: Optional[str] = Field(default=None)
|
|
flag_type: FeatureFlagType = Field(default=FeatureFlagType.SINGLE_VARIANT)
|
|
is_persist: Optional[bool] = Field(default=False)
|
|
is_active: Optional[bool] = Field(default=True)
|
|
conditions: List[FeatureFlagCondition] = Field(default=[], min_length=1)
|
|
variants: List[FeatureFlagVariant] = Field(default=[])
|
|
|
|
|
|
class ModuleStatus(BaseModel):
|
|
module: Literal["assist", "notes", "bug-reports",
|
|
"offline-recordings", "alerts", "assist-statts", "recommendations", "feature-flags"] = Field(...,
|
|
description="Possible values: assist, notes, bug-reports, offline-recordings, alerts, assist-statts, recommendations, feature-flags")
|
|
status: bool = Field(...)
|
|
|
|
|
|
class TagUpdate(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=100, pattern='^[a-zA-Z0-9\" -]*$')
|
|
|
|
|
|
class TagCreate(TagUpdate):
|
|
selector: str = Field(..., min_length=1, max_length=255)
|
|
ignoreClickRage: bool = Field(default=False)
|
|
ignoreDeadClick: bool = Field(default=False)
|
|
|
|
|
|
class ScopeType(str, Enum):
|
|
FULL_OR = "full"
|
|
SPOT_ONLY = "spot"
|
|
|
|
|
|
class ScopeSchema(BaseModel):
|
|
scope: ScopeType = Field(default=ScopeType.FULL_OR)
|