Coverage for schemas/schemas.py: 73%

805 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:56 +0000

1from typing import Annotated, List, Literal, Optional, Union 

2 

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 

15 

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) 

29 

30 

31class _GRecaptcha(BaseModel): 

32 g_recaptcha_response: Optional[str] = Field( 

33 default=None, alias="g-recaptcha-response" 

34 ) 

35 

36 

37class UserLoginSchema(_GRecaptcha): 

38 email: EmailStr = Field(...) 

39 password: SecretStr = Field(...) 

40 

41 _transform_email = field_validator("email", mode="before")(transform_email) 

42 

43 

44class UserSignupSchema(UserLoginSchema): 

45 fullname: str = Field(..., min_length=1) 

46 organizationName: str = Field(..., min_length=1) 

47 

48 _transform_fullname = field_validator("fullname", mode="before")(remove_whitespace) 

49 _transform_organizationName = field_validator("organizationName", mode="before")( 

50 remove_whitespace 

51 ) 

52 

53 _check_account_name = field_validator("fullname", "organizationName")( 

54 check_account_name 

55 ) 

56 

57 

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) 

62 

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) 

68 

69 

70class ForgetPasswordPayloadSchema(_GRecaptcha): 

71 email: EmailStr = Field(...) 

72 

73 _transform_email = field_validator("email", mode="before")(transform_email) 

74 

75 

76class EditUserPasswordSchema(BaseModel): 

77 old_password: SecretStr = Field(...) 

78 new_password: SecretStr = Field(...) 

79 

80 

81class CreateProjectSchema(BaseModel): 

82 name: str = Field(default="my first project", pattern=NAME_PATTERN) 

83 platform: Literal["web", "ios"] = Field(default="web") 

84 

85 _transform_name = field_validator("name", mode="before")(remove_whitespace) 

86 

87 

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(...) 

93 

94 

95class CurrentAPIContext(BaseModel): 

96 tenant_id: int = Field(...) 

97 project: Optional[ProjectContext] = Field(default=None) 

98 

99 

100class CurrentContext(CurrentAPIContext): 

101 user_id: int = Field(...) 

102 email: EmailStr = Field(...) 

103 role: str = Field(...) 

104 

105 _transform_email = field_validator("email", mode="before")(transform_email) 

106 

107 @computed_field 

108 @property 

109 def is_owner(self) -> bool: 

110 return self.role == "owner" 

111 

112 @computed_field 

113 @property 

114 def is_admin(self) -> bool: 

115 return self.role == "admin" 

116 

117 @computed_field 

118 @property 

119 def is_member(self) -> bool: 

120 return self.role == "member" 

121 

122 

123class AddCollaborationSchema(BaseModel): 

124 name: str = Field(..., pattern=NAME_PATTERN) 

125 url: HttpUrl = Field(...) 

126 

127 _transform_name = field_validator("name", mode="before")(remove_whitespace) 

128 _transform_url = field_validator("url", mode="before")(remove_whitespace) 

129 

130 

131class EditCollaborationSchema(AddCollaborationSchema): 

132 name: Optional[str] = Field(default=None, pattern=NAME_PATTERN) 

133 

134 

135class _TimedSchema(BaseModel): 

136 startTimestamp: int = Field(default=None) 

137 endTimestamp: int = Field(default=None) 

138 

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 

147 

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 

161 

162 

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) 

167 

168 

169class IssueTrackingIntegration(BaseModel): 

170 token: str = Field(...) 

171 

172 

173class IssueTrackingGithubSchema(IssueTrackingIntegration): 

174 pass 

175 

176 

177class IssueTrackingJiraSchema(IssueTrackingIntegration): 

178 username: str = Field(...) 

179 url: HttpUrl = Field(...) 

180 

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()) 

185 

186 

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) 

193 

194 _transform_name = field_validator("name", mode="before")(remove_whitespace) 

195 

196 

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) 

202 

203 _transform_email = field_validator("email", mode="before")(transform_email) 

204 _transform_name = field_validator("name", mode="before")(remove_whitespace) 

205 

206 

207class EditMemberSchema(BaseModel): 

208 name: str = Field(...) 

209 email: EmailStr = Field(...) 

210 admin: bool = Field(default=False) 

211 

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) 

215 

216 

217class EditPasswordByInvitationSchema(BaseModel): 

218 invitation: str = Field(...) 

219 passphrase: str = Field(..., alias="pass") 

220 password: SecretStr = Field(...) 

221 

222 

223class AssignmentSchema(BaseModel): 

224 assignee: str = Field(...) 

225 description: str = Field(...) 

226 title: str = Field(...) 

227 issue_type: str = Field(...) 

228 

229 _transform_title = field_validator("title", mode="before")(remove_whitespace) 

230 

231 

232class CommentAssignmentSchema(BaseModel): 

233 message: str = Field(...) 

234 

235 

236class IntegrationNotificationSchema(BaseModel): 

237 comment: Optional[str] = Field(default=None) 

238 

239 

240class GdprSchema(BaseModel): 

241 maskEmails: bool = Field(...) 

242 sampleRate: int = Field(...) 

243 maskNumbers: bool = Field(...) 

244 defaultInputMode: str = Field(...) 

245 

246 

247class SampleRateSchema(BaseModel): 

248 rate: int = Field(..., ge=0, le=100) 

249 capture_all: bool = Field(default=False) 

250 

251 

252class WeeklyReportConfigSchema(BaseModel): 

253 weekly_report: bool = Field(default=True) 

254 

255 

256class IntegrationBase(BaseModel): 

257 pass 

258 

259 

260class IntegrationSentrySchema(IntegrationBase): 

261 project_slug: str = Field(...) 

262 organization_slug: str = Field(...) 

263 token: str = Field(...) 

264 

265 

266class IntegrationDatadogSchema(IntegrationBase): 

267 api_key: str = Field(...) 

268 application_key: str = Field(...) 

269 

270 

271class IntegartionStackdriverSchema(IntegrationBase): 

272 service_account_credentials: str = Field(...) 

273 log_name: str = Field(...) 

274 

275 

276class IntegrationNewrelicSchema(IntegrationBase): 

277 application_id: str = Field(...) 

278 x_query_key: str = Field(...) 

279 region: bool = Field(default=False) 

280 

281 

282class IntegrationRollbarSchema(IntegrationBase): 

283 access_token: str = Field(...) 

284 

285 

286class IntegrationBugsnagBasicSchema(IntegrationBase): 

287 authorization_token: str = Field(...) 

288 

289 

290class IntegrationBugsnagSchema(IntegrationBugsnagBasicSchema): 

291 bugsnag_project_id: str = Field(...) 

292 

293 

294class IntegrationCloudwatchBasicSchema(IntegrationBase): 

295 aws_access_key_id: str = Field(...) 

296 aws_secret_access_key: str = Field(...) 

297 region: str = Field(...) 

298 

299 

300class IntegrationCloudwatchSchema(IntegrationCloudwatchBasicSchema): 

301 log_group_name: str = Field(...) 

302 

303 

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(...) 

309 

310 

311class IntegrationElasticsearchSchema(IntegrationElasticsearchTestSchema): 

312 indexes: str = Field(...) 

313 

314 

315class IntegrationSumologicSchema(IntegrationBase): 

316 access_id: str = Field(...) 

317 access_key: str = Field(...) 

318 region: str = Field(...) 

319 

320 

321class MetadataSchema(BaseModel): 

322 index: Optional[int] = Field(default=None) 

323 key: str = Field(...) 

324 

325 _transform_key = field_validator("key", mode="before")(remove_whitespace) 

326 

327 

328class _AlertMessageSchema(BaseModel): 

329 type: str = Field(...) 

330 value: str = Field(...) 

331 

332 _transform_value = field_validator("value", mode="before")(int_to_string) 

333 

334 

335class AlertDetectionType(str, Enum): 

336 PERCENT = "percent" 

337 CHANGE = "change" 

338 

339 

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) 

346 

347 

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" 

363 

364 

365class MathOperator(str, Enum): 

366 EQUAL = "=" 

367 LESS = "<" 

368 GREATER = ">" 

369 LESS_EQ = "<=" 

370 GREATER_EQ = ">=" 

371 

372 

373class _AlertQuerySchema(BaseModel): 

374 left: Union[AlertColumn, int] = Field(...) 

375 right: float = Field(...) 

376 operator: MathOperator = Field(...) 

377 

378 

379class AlertDetectionMethod(str, Enum): 

380 THRESHOLD = "threshold" 

381 CHANGE = "change" 

382 

383 

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) 

392 

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 

399 

400 return self 

401 

402 

403class SourcemapUploadPayloadSchema(BaseModel): 

404 urls: List[str] = Field(..., alias="URL") 

405 

406 

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" 

417 

418 

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" 

440 

441 

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" 

450 

451 

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" 

489 

490 

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" 

504 

505 

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" 

515 

516 

517class PlatformType(str, Enum): 

518 MOBILE = "mobile" 

519 DESKTOP = "desktop" 

520 TABLET = "tablet" 

521 

522 

523class SearchEventOrder(str, Enum): 

524 THEN = "then" 

525 OR = "or" 

526 AND = "and" 

527 

528 

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" 

545 

546 

547class MetricFormatType(str, Enum): 

548 SESSION_COUNT = "sessionCount" 

549 

550 

551class MetricExtendedFormatType(str, Enum): 

552 SESSION_COUNT = "sessionCount" 

553 USER_COUNT = "userCount" 

554 

555 

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" 

563 

564 

565class GraphqlFilterType(str, Enum): 

566 GRAPHQL_NAME = "graphqlName" 

567 GRAPHQL_METHOD = "graphqlMethod" 

568 GRAPHQL_REQUEST_BODY = "graphqlRequestBody" 

569 GRAPHQL_RESPONSE_BODY = "graphqlResponseBody" 

570 

571 

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(...) 

578 

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 

590 

591 

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" 

624 

625 

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" 

635 

636 

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) 

644 

645 # property_type: Optional[Literal["string", "number", "date"]] = Field(default=None) 

646 

647 @computed_field 

648 @property 

649 def is_predefined(self) -> bool: 

650 return EventPredefinedPropertyType.has_value(self.name) 

651 

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 

660 

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 

667 

668 

669class EventPropertiesSchema(BaseModel): 

670 operator: Literal["and", "or"] = Field(...) 

671 filters: List[PropertyFilterSchema] = Field(...) 

672 

673 

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) 

684 

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) 

689 

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 

695 

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 ) 

723 

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 

729 

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 

736 

737 

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) 

749 

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) 

754 

755 @computed_field 

756 @property 

757 def is_predefined(self) -> bool: 

758 return FilterType.has_value(self.name) 

759 

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 

772 

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 ) 

802 

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 ) 

816 

817 return self 

818 

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 

825 

826 

827class _PaginatedSchema(BaseModel): 

828 limit: int = Field(default=200, gt=0, le=200) 

829 page: int = Field(default=1, gt=0) 

830 

831 

832class SortOrderType(str, Enum): 

833 ASC = "ASC" 

834 DESC = "DESC" 

835 

836 

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 

845 

846 

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] 

853 

854 

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) 

865 

866 @model_validator(mode="before") 

867 @classmethod 

868 def transform_order(cls, values): 

869 if values.get("sort") is None: 

870 values["sort"] = "startTs" 

871 

872 if values.get("order") is None: 

873 values["order"] = SortOrderType.DESC 

874 else: 

875 values["order"] = values["order"].upper() 

876 return values 

877 

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 

889 

890 # in case the old search payload was passed 

891 for v in values.get("events") or []: 

892 v["isEvent"] = True 

893 

894 return values 

895 

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 

912 

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 

924 

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 

937 

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 

965 

966 return values 

967 

968 

969class ErrorStatus(str, Enum): 

970 ALL = "all" 

971 UNRESOLVED = "unresolved" 

972 RESOLVED = "resolved" 

973 IGNORED = "ignored" 

974 

975 

976class ErrorSort(str, Enum): 

977 LAST_OCCURRENCE = "lastOccurrence" 

978 FIRST_OCCURRENCE = "firstOccurrence" 

979 USERS_COUNT = "users" 

980 SESSIONS_COUNT = "sessions" 

981 

982 

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) 

988 

989 

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 

995 

996 

997class MobileSignPayloadSchema(BaseModel): 

998 keys: List[str] = Field(...) 

999 

1000 

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) 

1006 

1007 

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) 

1012 

1013 

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" 

1022 

1023 

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" 

1042 

1043 

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 ) 

1051 

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 

1060 

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 

1067 

1068 

1069class LiveSessionsSearchPayloadSchema(_PaginatedSchema): 

1070 filters: List[LiveSessionSearchFilterSchema] = Field([]) 

1071 sort: Union[LiveFilterType, str] = Field(default="TIMESTAMP") 

1072 order: SortOrderType = Field(default=SortOrderType.DESC) 

1073 

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 

1096 

1097 

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" 

1113 

1114 

1115class WebhookType(str, Enum): 

1116 WEBHOOK = "webhook" 

1117 SLACK = "slack" 

1118 EMAIL = "email" 

1119 MSTEAMS = "msteams" 

1120 

1121 

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" 

1132 

1133 

1134class ModuleStatus(BaseModel): 

1135 module: ModuleType = Field(...) 

1136 status: bool = Field(...) 

1137 

1138 

1139class TagUpdate(BaseModel): 

1140 name: str = Field(..., min_length=1, max_length=100, pattern='^[a-zA-Z0-9" -]*$') 

1141 

1142 

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) 

1147 

1148 

1149class ScopeSchema(BaseModel): 

1150 scope: int = Field(default=1, ge=1, le=2) 

1151 

1152 

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)