1010import hashlib
1111import re
1212import traceback
13- from typing import TYPE_CHECKING , Annotated
13+ from datetime import datetime
14+ from typing import Annotated , Final , TypeAlias
1415
16+ import arrow
1517from pydantic import StringConstraints , TypeAdapter
1618
17- _LABEL = "OEC:{}"
18- _PATTERN = r"OEC:[a-zA-Z0-9]+"
19-
20- if TYPE_CHECKING :
21- ErrorCodeStr = str
22- else :
23- ErrorCodeStr = Annotated [
24- str , StringConstraints (strip_whitespace = True , pattern = _PATTERN )
25- ]
19+ _LABEL = "OEC:{fingerprint}-{timestamp}"
2620
2721_LEN = 12 # chars (~48 bits)
22+ _NAMED_PATTERN = re .compile (
23+ r"OEC:(?P<fingerprint>[a-fA-F0-9]{12})-(?P<timestamp>\d{13,14})"
24+ # NOTE: timestamp limits: 13 digits (from 2001), 14 digits (good for ~500+ years)
25+ )
26+ _PATTERN = re .compile (r"OEC:[a-fA-F0-9]{12}-\d{13,14}" )
27+
2828
29+ ErrorCodeStr : TypeAlias = Annotated [
30+ str , StringConstraints (strip_whitespace = True , pattern = _NAMED_PATTERN )
31+ ]
2932
30- def _generate_error_fingerprint (exc : BaseException ) -> str :
33+
34+ def _create_fingerprint (exc : BaseException ) -> str :
3135 """
32- Unique error fingerprint for deduplication purposes
36+ Unique error fingerprint of the **traceback** for deduplication purposes
3337 """
3438 tb = traceback .extract_tb (exc .__traceback__ )
3539 frame_sigs = [f"{ frame .name } :{ frame .lineno } " for frame in tb ]
@@ -38,11 +42,43 @@ def _generate_error_fingerprint(exc: BaseException) -> str:
3842 return hashlib .sha256 (fingerprint .encode ()).hexdigest ()[:_LEN ]
3943
4044
45+ _MILISECONDS : Final [int ] = 1000
46+
47+
48+ def _create_timestamp () -> int :
49+ """Timestamp as milliseconds since epoch
50+ NOTE: this reduces the precission to milliseconds but it is good enough for our purpose
51+ """
52+ ts = arrow .utcnow ().float_timestamp * _MILISECONDS
53+ return int (ts )
54+
55+
4156def create_error_code (exception : BaseException ) -> ErrorCodeStr :
57+ """
58+ Generates a unique error code for the given exception.
59+
60+ The error code follows the format: `OEC:{traceback}-{timestamp}`.
61+ This code is intended to be shared with the front-end as a `SupportID`
62+ for debugging and support purposes.
63+ """
4264 return TypeAdapter (ErrorCodeStr ).validate_python (
43- _LABEL .format (_generate_error_fingerprint (exception ))
65+ _LABEL .format (
66+ fingerprint = _create_fingerprint (exception ),
67+ timestamp = _create_timestamp (),
68+ )
4469 )
4570
4671
47- def parse_error_code (obj ) -> set [ErrorCodeStr ]:
48- return set (re .findall (_PATTERN , f"{ obj } " ))
72+ def parse_error_codes (obj ) -> list [ErrorCodeStr ]:
73+ return TypeAdapter (list [ErrorCodeStr ]).validate_python (_PATTERN .findall (f"{ obj } " ))
74+
75+
76+ def parse_error_code_parts (oec : ErrorCodeStr ) -> tuple [str , datetime ]:
77+ """Returns traceback-fingerprint and timestamp from `OEC:{traceback}-{timestamp}`"""
78+ match = _NAMED_PATTERN .match (oec )
79+ if not match :
80+ msg = f"Invalid error code format: { oec } "
81+ raise ValueError (msg )
82+ fingerprint = match .group ("fingerprint" )
83+ timestamp = arrow .get (int (match .group ("timestamp" )) / _MILISECONDS ).datetime
84+ return fingerprint , timestamp
0 commit comments