• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #4681

pending completion
#4681

push

coveralls-python

suhaibmujahid
[uplift_beta] Alert about potential out-of-sync uplift flags

716 of 3562 branches covered (20.1%)

2 of 2 new or added lines in 1 file covered. (100.0%)

1924 of 8721 relevant lines covered (22.06%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/bugbot/crash/analyzer.py
1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
import itertools
×
6
import re
×
7
from collections import defaultdict
×
8
from datetime import date, timedelta
×
9
from functools import cached_property
×
10
from typing import Iterable, Iterator
×
11

12
from libmozdata import bugzilla, clouseau, connection, socorro
×
13
from libmozdata import utils as lmdutils
×
14
from libmozdata.bugzilla import Bugzilla
×
15
from libmozdata.connection import Connection
×
16

17
from bugbot import logger, utils
×
18
from bugbot.bug.analyzer import BugAnalyzer, BugsStore
×
19
from bugbot.components import ComponentName
×
20
from bugbot.crash import socorro_util
×
21

22
# The max offset from a memory address to be considered "near".
23
OFFSET_64_BIT = 0x1000
×
24
OFFSET_32_BIT = 0x100
×
25
# Allocator poison value addresses.
26
ALLOCATOR_ADDRESSES_64_BIT = (
×
27
    (0xE5E5E5E5E5E5E5E5, OFFSET_64_BIT),
28
    # On 64-bit windows, sometimes it could be doing something with a 32-bit
29
    # value gotten from freed memory, so it'll be 0X00000000E5E5E5E5 +/-, and
30
    # because of the address limitation, quite often it will be
31
    # 0X0000E5E5E5E5E5E5 +/-.
32
    (0x00000000E5E5E5E5, OFFSET_32_BIT),
33
    (0x0000E5E5E5E5E5E5, OFFSET_64_BIT),
34
    (0x4B4B4B4B4B4B4B4B, OFFSET_64_BIT),
35
)
36
ALLOCATOR_ADDRESSES_32_BIT = (
×
37
    (0xE5E5E5E5, OFFSET_32_BIT),
38
    (0x4B4B4B4B, OFFSET_32_BIT),
39
)
40
# Ranges where addresses are considered near allocator poison values.
41
ALLOCATOR_RANGES_64_BIT = (
×
42
    (addr - offset, addr + offset) for addr, offset in ALLOCATOR_ADDRESSES_64_BIT
43
)
44
ALLOCATOR_RANGES_32_BIT = (
×
45
    (addr - offset, addr + offset) for addr, offset in ALLOCATOR_ADDRESSES_32_BIT
46
)
47

48

49
def is_near_null_address(str_address) -> bool:
×
50
    """Check if the address is near null.
51

52
    Args:
53
        str_address: The memory address to check.
54

55
    Returns:
56
        True if the address is near null, False otherwise.
57
    """
58
    address = int(str_address, 0)
×
59
    is_64_bit = len(str_address) >= 18
×
60

61
    if is_64_bit:
×
62
        return -OFFSET_64_BIT <= address <= OFFSET_64_BIT
×
63

64
    return -OFFSET_32_BIT <= address <= OFFSET_32_BIT
×
65

66

67
def is_near_allocator_address(str_address) -> bool:
×
68
    """Check if the address is near an allocator poison value.
69

70
    Args:
71
        str_address: The memory address to check.
72

73
    Returns:
74
        True if the address is near an allocator poison value, False otherwise.
75
    """
76
    address = int(str_address, 0)
×
77
    is_64_bit = len(str_address) >= 18
×
78

79
    return any(
×
80
        low <= address <= high
81
        for low, high in (
82
            ALLOCATOR_RANGES_64_BIT if is_64_bit else ALLOCATOR_RANGES_32_BIT
83
        )
84
    )
85

86

87
# TODO: Move this to libmozdata
88
def generate_signature_page_url(params: dict, tab: str) -> str:
×
89
    """Generate a URL to the signature page on Socorro
90

91
    Args:
92
        params: the parameters for the search query.
93
        tab: the page tab that should be selected.
94

95
    Returns:
96
        The URL of the signature page on Socorro
97
    """
98
    web_url = socorro.Socorro.CRASH_STATS_URL
×
99
    query = lmdutils.get_params_for_url(params)
×
100
    return f"{web_url}/signature/{query}#{tab}"
×
101

102

103
# NOTE: At this point, we will file bugs on bugzilla-dev. Once we are confident
104
# that the bug filing is working as expected, we can switch to filing bugs in
105
# the production instance of Bugzilla.
106
class DevBugzilla(Bugzilla):
×
107
    URL = "https://bugzilla-dev.allizom.org"
×
108
    API_URL = URL + "/rest/bug"
×
109
    ATTACHMENT_API_URL = API_URL + "/attachment"
×
110
    TOKEN = utils.get_login_info()["bz_api_key_dev"]
×
111

112

113
class NoCrashReportFoundError(Exception):
×
114
    """There are no crash reports that meet the required criteria."""
115

116

117
class ClouseauDataAnalyzer:
×
118
    """Analyze the data returned by Crash Clouseau about a specific crash
119
    signature.
120
    """
121

122
    MINIMUM_CLOUSEAU_SCORE_THRESHOLD: int = 8
×
123
    DEFAULT_CRASH_COMPONENT = ComponentName("Core", "General")
×
124

125
    def __init__(self, reports: Iterable[dict], bugs_store: BugsStore):
×
126
        self._clouseau_reports = reports
×
127
        self.bugs_store = bugs_store
×
128

129
    @cached_property
×
130
    def max_clouseau_score(self):
×
131
        """The maximum Clouseau score in the crash reports."""
132
        if not self._clouseau_reports:
×
133
            return 0
×
134
        return max(report["max_score"] for report in self._clouseau_reports)
×
135

136
    @cached_property
×
137
    def regressed_by_potential_bug_ids(self) -> set[int]:
×
138
        """The IDs for the bugs that their patches could have caused the crash."""
139
        minimum_accepted_score = max(
×
140
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
141
        )
142
        return {
×
143
            changeset["bug_id"]
144
            for report in self._clouseau_reports
145
            if report["max_score"] >= minimum_accepted_score
146
            for changeset in report["changesets"]
147
            if changeset["max_score"] >= minimum_accepted_score
148
            and not changeset["is_merge"]
149
            and not changeset["is_backedout"]
150
        }
151

152
    @cached_property
×
153
    def regressed_by_patch(self) -> str | None:
×
154
        """The hash of the patch that could have caused the crash."""
155
        minimum_accepted_score = max(
×
156
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
157
        )
158
        potential_patches = {
×
159
            changeset["changeset"]
160
            for report in self._clouseau_reports
161
            if report["max_score"] >= minimum_accepted_score
162
            for changeset in report["changesets"]
163
            if changeset["max_score"] >= minimum_accepted_score
164
            and not changeset["is_merge"]
165
            and not changeset["is_backedout"]
166
        }
167
        if len(potential_patches) == 1:
×
168
            return next(iter(potential_patches))
×
169
        return None
×
170

171
    @cached_property
×
172
    def regressed_by(self) -> int | None:
×
173
        """The ID of the bug that one of its patches could have caused
174
        the crash.
175

176
        If there are multiple bugs, the value will be `None`.
177
        """
178
        bug_ids = self.regressed_by_potential_bug_ids
×
179
        if len(bug_ids) == 1:
×
180
            return next(iter(bug_ids))
×
181
        return None
×
182

183
    @cached_property
×
184
    def regressed_by_potential_bugs(self) -> list[BugAnalyzer]:
×
185
        """The bugs whose patches could have caused the crash."""
186
        self.bugs_store.fetch_bugs(
×
187
            self.regressed_by_potential_bug_ids,
188
            [
189
                "id",
190
                "groups",
191
                "assigned_to",
192
                "product",
193
                "component",
194
            ],
195
        )
196
        return [
×
197
            self.bugs_store.get_bug_by_id(bug_id)
198
            for bug_id in self.regressed_by_potential_bug_ids
199
        ]
200

201
    @cached_property
×
202
    def regressed_by_author(self) -> dict | None:
×
203
        """The author of the patch that could have caused the crash.
204

205
        If there are multiple regressors, the value will be `None`.
206

207
        The regressor bug assignee is considered as the author, even if the
208
        assignee is not the patch author.
209
        """
210

211
        if not self.regressed_by:
×
212
            return None
×
213

214
        bug = self.regressed_by_potential_bugs[0]
×
215
        assert bug.id == self.regressed_by
×
216
        return bug.get_field("assigned_to_detail")
×
217

218
    @cached_property
×
219
    def crash_component(self) -> ComponentName:
×
220
        """The component that the crash belongs to.
221

222
        If there are multiple components, the value will be the default one.
223
        """
224
        potential_components = {
×
225
            bug.component for bug in self.regressed_by_potential_bugs
226
        }
227
        if len(potential_components) == 1:
×
228
            return next(iter(potential_components))
×
229
        return self.DEFAULT_CRASH_COMPONENT
×
230

231

232
class SocorroDataAnalyzer(socorro_util.SignatureStats):
×
233
    """Analyze the data returned by Socorro."""
234

235
    _bugzilla_os_legal_values = None
×
236
    _bugzilla_cpu_legal_values_map = None
×
237
    _platforms = [
×
238
        {"short_name": "win", "name": "Windows"},
239
        {"short_name": "mac", "name": "Mac OS X"},
240
        {"short_name": "lin", "name": "Linux"},
241
        {"short_name": "and", "name": "Android"},
242
        {"short_name": "unknown", "name": "Unknown"},
243
    ]
244

245
    def __init__(
×
246
        self,
247
        signature: dict,
248
        num_total_crashes: int,
249
    ):
250
        super().__init__(signature, num_total_crashes, platforms=self._platforms)
×
251

252
    @classmethod
×
253
    def to_bugzilla_op_sys(cls, op_sys: str) -> str:
×
254
        """Return the corresponding OS name in Bugzilla for the provided OS name
255
        from Socorro.
256

257
        If the OS name is not recognized, return "Other".
258
        """
259
        if cls._bugzilla_os_legal_values is None:
×
260
            cls._bugzilla_os_legal_values = set(
×
261
                bugzilla.BugFields.fetch_field_values("op_sys")
262
            )
263

264
        if op_sys in cls._bugzilla_os_legal_values:
×
265
            return op_sys
×
266

267
        if op_sys.startswith("OS X ") or op_sys.startswith("macOS "):
×
268
            op_sys = "macOS"
×
269
        elif op_sys.startswith("Windows"):
×
270
            op_sys = "Windows"
×
271
        elif "Linux" in op_sys or op_sys.startswith("Ubuntu"):
×
272
            op_sys = "Linux"
×
273
        else:
274
            op_sys = "Other"
×
275

276
        return op_sys
×
277

278
    @property
×
279
    def first_crash_date(self) -> str:
×
280
        """The date of the first crash within the query time range.
281

282
        The date is in YYYY-MM-DD format.
283
        """
284
        return self.signature["facets"]["histogram_date"][0]["term"][:10]
×
285

286
    @property
×
287
    def bugzilla_op_sys(self) -> str:
×
288
        """The name of the OS where the crash happens.
289

290
        The value is one of the legal values for Bugzilla's `op_sys` field.
291

292
        - If no OS name is found, the value will be "Unspecified".
293
        - If the OS name is not recognized, the value will be "Other".
294
        - If multiple OS names are found, the value will be "All". Unless the OS
295
          names can be resolved to a common name without a version. For example,
296
          "Windows 10" and "Windows 7" will become "Windows".
297
        """
298
        all_op_sys = {
×
299
            self.to_bugzilla_op_sys(op_sys["term"])
300
            for op_sys in self.signature["facets"]["platform_pretty_version"]
301
        }
302

303
        if len(all_op_sys) > 1:
×
304
            # Resolve to root OS name by removing the version number.
305
            all_op_sys = {op_sys.split(" ")[0] for op_sys in all_op_sys}
×
306

307
        if len(all_op_sys) == 2 and "Other" in all_op_sys:
×
308
            # TODO: explain this workaround.
309
            all_op_sys.remove("Other")
×
310

311
        if len(all_op_sys) == 1:
×
312
            return next(iter(all_op_sys))
×
313

314
        if len(all_op_sys) == 0:
×
315
            return "Unspecified"
×
316

317
        return "All"
×
318

319
    @classmethod
×
320
    def to_bugzilla_cpu(cls, cpu: str) -> str:
×
321
        """Return the corresponding CPU name in Bugzilla for the provided name
322
        from Socorro.
323

324
        If the CPU is not recognized, return "Other".
325
        """
326
        if cls._bugzilla_cpu_legal_values_map is None:
×
327
            cls._bugzilla_cpu_legal_values_map = {
×
328
                value.lower(): value
329
                for value in bugzilla.BugFields.fetch_field_values("rep_platform")
330
            }
331

332
        return cls._bugzilla_cpu_legal_values_map.get(cpu, "Other")
×
333

334
    @property
×
335
    def bugzilla_cpu_arch(self) -> str:
×
336
        """The CPU architecture of the devices where the crash happens.
337

338
        The value is one of the legal values for Bugzilla's `rep_platform` field.
339

340
        - If no CPU architecture is found, the value will be "Unspecified".
341
        - If the CPU architecture is not recognized, the value will be "Other".
342
        - If multiple CPU architectures are found, the value will "All".
343
        """
344
        all_cpu_arch = {
×
345
            self.to_bugzilla_cpu(cpu["term"])
346
            for cpu in self.signature["facets"]["cpu_arch"]
347
        }
348

349
        if len(all_cpu_arch) == 2 and "Other" in all_cpu_arch:
×
350
            all_cpu_arch.remove("Other")
×
351

352
        if len(all_cpu_arch) == 1:
×
353
            return next(iter(all_cpu_arch))
×
354

355
        if len(all_cpu_arch) == 0:
×
356
            return "Unspecified"
×
357

358
        return "All"
×
359

360
    @property
×
361
    def user_comments_page_url(self) -> str:
×
362
        """The URL to the Signature page on Socorro where the Comments tab is
363
        selected.
364
        """
365
        start_date = date.today() - timedelta(weeks=26)
×
366
        params = {
×
367
            "signature": self.signature_term,
368
            "date": socorro.SuperSearch.get_search_date(start_date),
369
        }
370
        return generate_signature_page_url(params, "comments")
×
371

372
    @property
×
373
    def num_user_comments(self) -> int:
×
374
        """The number of crash reports with user comments."""
375
        # TODO: count useful/interesting user comments (e.g., exclude one word comments)
376
        return self.signature["facets"]["cardinality_user_comments"]["value"]
×
377

378
    @property
×
379
    def has_user_comments(self) -> bool:
×
380
        """Whether the crash signature has any reports with a user comment."""
381
        return self.num_user_comments > 0
×
382

383
    @property
×
384
    def top_proto_signature(self) -> str:
×
385
        """The proto signature that occurs the most."""
386
        return self.signature["facets"]["proto_signature"][0]["term"]
×
387

388
    @property
×
389
    def num_top_proto_signature_crashes(self) -> int:
×
390
        """The number of crashes for the most occurring proto signature."""
391
        return self.signature["facets"]["proto_signature"][0]["count"]
×
392

393
    def _build_ids(self) -> Iterator[int]:
×
394
        """Yields the build IDs where the crash occurred."""
395
        for build_id in self.signature["facets"]["build_id"]:
×
396
            yield build_id["term"]
×
397

398
    @property
×
399
    def top_build_id(self) -> int:
×
400
        """The build ID where most crashes occurred."""
401
        return self.signature["facets"]["build_id"][0]["term"]
×
402

403
    @cached_property
×
404
    def num_near_null_crashes(self) -> int:
×
405
        """The number of crashes that occurred on addresses near null."""
406
        return sum(
×
407
            address["count"]
408
            for address in self.signature["facets"]["address"]
409
            if is_near_null_address(address["term"])
410
        )
411

412
    @property
×
413
    def is_near_null_crash(self) -> bool:
×
414
        """Whether all crashes occurred on addresses near null."""
415
        return self.num_near_null_crashes == self.num_crashes
×
416

417
    @property
×
418
    def is_potential_near_null_crash(self) -> bool:
×
419
        """Whether the signature is a potential near null crash.
420

421
        The value will be True if some but not all crashes occurred on addresses
422
        near null.
423
        """
424
        return not self.is_near_null_crash and self.num_near_null_crashes > 0
×
425

426
    @property
×
427
    def is_near_null_related_crash(self) -> bool:
×
428
        """Whether the signature is related to near null crashes.
429

430
        The value will be True if any of the crashes occurred on addresses near
431
        null.
432
        """
433
        return self.is_near_null_crash or self.is_potential_near_null_crash
×
434

435
    @cached_property
×
436
    def num_near_allocator_crashes(self) -> int:
×
437
        """The number of crashes that occurred on addresses near an allocator
438
        poison value.
439
        """
440
        return sum(
×
441
            address["count"]
442
            for address in self.signature["facets"]["address"]
443
            if is_near_allocator_address(address["term"])
444
        )
445

446
    @property
×
447
    def is_near_allocator_crash(self) -> bool:
×
448
        """Whether all crashes occurred on addresses near an allocator poison
449
        value.
450
        """
451
        return self.num_near_allocator_crashes == self.num_crashes
×
452

453
    @property
×
454
    def is_potential_near_allocator_crash(self) -> bool:
×
455
        """Whether the signature is a potential near allocator poison value
456
        crash.
457

458
        The value will be True if some but not all crashes occurred on addresses
459
        near an allocator poison value.
460
        """
461
        return not self.is_near_allocator_crash and self.num_near_allocator_crashes > 0
×
462

463
    @property
×
464
    def is_near_allocator_related_crash(self) -> bool:
×
465
        """Whether the signature is related to near allocator poison value
466
        crashes.
467

468
        The value will be True if any of the crashes occurred on addresses near
469
        an allocator poison value.
470
        """
471
        return self.is_near_allocator_crash or self.is_potential_near_allocator_crash
×
472

473

474
class SignatureAnalyzer(SocorroDataAnalyzer, ClouseauDataAnalyzer):
×
475
    """Analyze the data related to a signature.
476

477
    This includes data from Socorro and Clouseau.
478
    """
479

480
    def __init__(
×
481
        self,
482
        socorro_signature: dict,
483
        num_total_crashes: int,
484
        clouseau_reports: list[dict],
485
        bugs_store: BugsStore,
486
    ):
487
        SocorroDataAnalyzer.__init__(self, socorro_signature, num_total_crashes)
×
488
        ClouseauDataAnalyzer.__init__(self, clouseau_reports, bugs_store)
×
489

490
    def _fetch_crash_reports(
×
491
        self,
492
        proto_signature: str,
493
        build_id: int | Iterable[int],
494
        limit: int = 1,
495
    ) -> Iterator[dict]:
496
        params = {
×
497
            "proto_signature": "=" + proto_signature,
498
            "build_id": build_id,
499
            "_columns": [
500
                "uuid",
501
            ],
502
            "_results_number": limit,
503
        }
504

505
        def handler(res: dict, data: dict):
×
506
            data.update(res)
×
507

508
        data: dict = {}
×
509
        socorro.SuperSearch(params=params, handler=handler, handlerdata=data).wait()
×
510

511
        yield from data["hits"]
×
512

513
    def fetch_representative_processed_crash(self) -> dict:
×
514
        """Fetch a processed crash to represent the signature.
515

516
        This could fetch multiple processed crashes and return the one that is
517
        most likely to be useful.
518
        """
519
        limit_to_top_proto_signature = (
×
520
            self.num_top_proto_signature_crashes / self.num_crashes > 0.6
521
        )
522

523
        reports = itertools.chain(
×
524
            # Reports with a higher score from clouseau are more likely to be
525
            # useful.
526
            sorted(
527
                self._clouseau_reports,
528
                key=lambda report: report["max_score"],
529
                reverse=True,
530
            ),
531
            # Next we try find reports from the top crashing build because they
532
            # are likely to be representative.
533
            self._fetch_crash_reports(self.top_proto_signature, self.top_build_id),
534
            self._fetch_crash_reports(self.top_proto_signature, self._build_ids()),
535
        )
536
        for report in reports:
×
537
            uuid = report["uuid"]
×
538
            processed_crash = socorro.ProcessedCrash.get_processed(uuid)[uuid]
×
539
            if (
×
540
                not limit_to_top_proto_signature
541
                or processed_crash["proto_signature"] == self.top_proto_signature
542
            ):
543
                # TODO(investigate): maybe we should check if the stack is
544
                # corrupted (ask gsvelto or willkg about how to detect that)
545
                return processed_crash
×
546

547
        raise NoCrashReportFoundError(
×
548
            f"No crash report found with the most frequent proto signature for {self.signature_term}."
549
        )
550

551
    @cached_property
×
552
    def is_potential_security_crash(self) -> bool:
×
553
        """Whether the crash is related to a potential security bug.
554

555
        The value will be True if:
556
            - the signature is related to near allocator poison value crashes, or
557
            - one of the potential regressors is a security bug
558
        """
559
        return self.is_near_allocator_related_crash or any(
×
560
            bug.is_security for bug in self.regressed_by_potential_bugs
561
        )
562

563

564
class SignaturesDataFetcher:
×
565
    """Fetch the data related to the given signatures."""
566

567
    MEMORY_ACCESS_ERROR_REASONS = (
×
568
        # On Windows:
569
        "EXCEPTION_ACCESS_VIOLATION_READ",
570
        "EXCEPTION_ACCESS_VIOLATION_WRITE",
571
        "EXCEPTION_ACCESS_VIOLATION_EXEC"
572
        # On Linux:
573
        "SIGSEGV / SEGV_MAPERR",
574
        "SIGSEGV / SEGV_ACCERR",
575
    )
576

577
    EXCLUDED_MOZ_REASON_STRINGS = (
×
578
        "MOZ_CRASH(OOM)",
579
        "MOZ_CRASH(Out of memory)",
580
        "out of memory",
581
        "Shutdown hanging",
582
        # TODO(investigate): do we need to exclude signatures that their reason
583
        # contains `[unhandlable oom]`?
584
        # Example: arena_t::InitChunk | arena_t::AllocRun | arena_t::MallocLarge | arena_t::Malloc | BaseAllocator::malloc | Allocator::malloc | PageMalloc
585
        # "[unhandlable oom]",
586
    )
587

588
    # If any of the crash reason starts with any of the following, then it is
589
    # Network or I/O error.
590
    EXCLUDED_IO_ERROR_REASON_PREFIXES = (
×
591
        "EXCEPTION_IN_PAGE_ERROR_READ",
592
        "EXCEPTION_IN_PAGE_ERROR_WRITE",
593
        "EXCEPTION_IN_PAGE_ERROR_EXEC",
594
    )
595

596
    # TODO(investigate): do we need to exclude all these signatures prefixes?
597
    EXCLUDED_SIGNATURE_PREFIXES = (
×
598
        "OOM | ",
599
        "bad hardware | ",
600
        "shutdownhang | ",
601
    )
602

603
    SUMMARY_DURATION = timedelta(weeks=10)
×
604

605
    def __init__(
×
606
        self,
607
        signatures: Iterable[str],
608
        product: str = "Firefox",
609
        channel: str = "nightly",
610
    ):
611
        self._signatures = set(signatures)
×
612
        self._product = product
×
613
        self._channel = channel
×
614

615
    @classmethod
×
616
    def find_new_actionable_crashes(
×
617
        cls,
618
        product: str,
619
        channel: str,
620
        days_to_check: int = 7,
621
        days_without_crashes: int = 7,
622
    ) -> "SignaturesDataFetcher":
623
        """Find new actionable crashes.
624

625
        Args:
626
            product: The product to check.
627
            channel: The release channel to check.
628
            days_to_check: The number of days to check for crashes.
629
            days_without_crashes: The number of days without crashes before the
630
                `days_to_check` to consider the signature new.
631

632
        Returns:
633
            A list of actionable signatures.
634
        """
635
        duration = days_to_check + days_without_crashes
×
636
        end_date = lmdutils.get_date_ymd("today")
×
637
        start_date = end_date - timedelta(duration)
×
638
        earliest_allowed_date = lmdutils.get_date_str(
×
639
            end_date - timedelta(days_to_check)
640
        )
641
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
642

643
        params = {
×
644
            "product": product,
645
            "release_channel": channel,
646
            "date": date_range,
647
            # TODO(investigate): should we do a local filter instead of the
648
            # following (should we exclude the signature if one of the crashes
649
            # is a shutdown hang?):
650
            # If the `ipc_shutdown_state` or `shutdown_progress` field are
651
            # non-empty then it's a shutdown hang.
652
            "ipc_shutdown_state": "__null__",
653
            "shutdown_progress": "__null__",
654
            # TODO(investigate): should we use the following instead of the
655
            # local filter.
656
            # "oom_allocation_size": "!__null__",
657
            "_aggs.signature": [
658
                "moz_crash_reason",
659
                "reason",
660
                "_histogram.date",
661
                "_cardinality.install_time",
662
                "_cardinality.oom_allocation_size",
663
            ],
664
            "_results_number": 0,
665
            "_facets_size": 10000,
666
        }
667

668
        def handler(search_resp: dict, data: list):
×
669
            logger.debug(
×
670
                "Total of %d signatures received from Socorro",
671
                len(search_resp["facets"]["signature"]),
672
            )
673

674
            for crash in search_resp["facets"]["signature"]:
×
675
                signature = crash["term"]
×
676
                if any(
×
677
                    signature.startswith(excluded_prefix)
678
                    for excluded_prefix in cls.EXCLUDED_SIGNATURE_PREFIXES
679
                ):
680
                    # Ignore signatures that start with any of the excluded prefixes.
681
                    continue
×
682

683
                facets = crash["facets"]
×
684
                installations = facets["cardinality_install_time"]["value"]
×
685
                if installations <= 1:
×
686
                    # Ignore crashes that only happen on one installation.
687
                    continue
×
688

689
                first_date = facets["histogram_date"][0]["term"]
×
690
                if first_date < earliest_allowed_date:
×
691
                    # The crash is not new, skip it.
692
                    continue
×
693

694
                if any(
×
695
                    reason["term"].startswith(io_error_prefix)
696
                    for reason in facets["reason"]
697
                    for io_error_prefix in cls.EXCLUDED_IO_ERROR_REASON_PREFIXES
698
                ):
699
                    # Ignore Network or I/O error crashes.
700
                    continue
×
701

702
                if crash["count"] < 20:
×
703
                    # For signatures with low volume, having multiple types of
704
                    # memory errors indicates potential bad hardware crashes.
705
                    num_memory_error_types = sum(
×
706
                        reason["term"] in cls.MEMORY_ACCESS_ERROR_REASONS
707
                        for reason in facets["reason"]
708
                    )
709
                    if num_memory_error_types > 1:
×
710
                        # Potential bad hardware crash, skip it.
711
                        continue
×
712

713
                # TODO: Add a filter using the `possible_bit_flips_max_confidence`
714
                # field to exclude bad hardware crashes. The filed is not available yet.
715
                # See: https://bugzilla.mozilla.org/show_bug.cgi?id=1816669#c3
716

717
                # TODO(investigate): is this needed since we are already
718
                # filtering signatures that start with "OOM | "
719
                if facets["cardinality_oom_allocation_size"]["value"]:
×
720
                    # If one of the crashes is an OOM crash, skip it.
721
                    continue
×
722

723
                # TODO(investigate): do we need to check for the `moz_crash_reason`
724
                moz_crash_reasons = facets["moz_crash_reason"]
×
725
                if moz_crash_reasons and any(
×
726
                    excluded_reason in reason["term"]
727
                    for reason in moz_crash_reasons
728
                    for excluded_reason in cls.EXCLUDED_MOZ_REASON_STRINGS
729
                ):
730
                    continue
×
731

732
                data.append(signature)
×
733

734
        signatures: list = []
×
735
        socorro.SuperSearch(
×
736
            params=params,
737
            handler=handler,
738
            handlerdata=signatures,
739
        ).wait()
740

741
        logger.debug(
×
742
            "Total of %d signatures left after applying the filtering criteria",
743
            len(signatures),
744
        )
745

746
        return cls(signatures, product, channel)
×
747

748
    def fetch_clouseau_crash_reports(self) -> dict[str, list]:
×
749
        """Fetch the crash reports data from Crash Clouseau."""
750
        if not self._signatures:
×
751
            return {}
×
752

753
        logger.debug(
×
754
            "Fetch from Clouseau: requesting reports for %d signatures",
755
            len(self._signatures),
756
        )
757

758
        signature_reports = clouseau.Reports.get_by_signatures(
×
759
            self._signatures,
760
            product=self._product,
761
            channel=self._channel,
762
        )
763

764
        logger.debug(
×
765
            "Fetch from Clouseau: received reports for %d signatures",
766
            len(signature_reports),
767
        )
768

769
        return signature_reports
×
770

771
    def fetch_socorro_info(self) -> tuple[list[dict], int]:
×
772
        """Fetch the signature data from Socorro."""
773
        if not self._signatures:
×
774
            return [], 0
×
775

776
        end_date = lmdutils.get_date_ymd("today")
×
777
        start_date = end_date - self.SUMMARY_DURATION
×
778
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
779

780
        params = {
×
781
            "product": self._product,
782
            # TODO(investigate): should we included all release channels?
783
            "release_channel": self._channel,
784
            # TODO(investigate): should we limit based on the build date as well?
785
            "date": date_range,
786
            # TODO: split signatures into chunks to avoid very long query URLs
787
            "signature": ["=" + signature for signature in self._signatures],
788
            "_aggs.signature": [
789
                "address",
790
                "build_id",
791
                "cpu_arch",
792
                "proto_signature",
793
                "_cardinality.user_comments",
794
                "cpu_arch",
795
                "platform_pretty_version",
796
                "_histogram.date",
797
                # The following are needed for SignatureStats:
798
                "platform",
799
                "is_garbage_collecting",
800
                "_cardinality.install_time",
801
                "startup_crash",
802
                "_histogram.uptime",
803
                "process_type",
804
            ],
805
            "_results_number": 0,
806
            "_facets_size": 10000,
807
        }
808

809
        def handler(search_results: dict, data: dict):
×
810
            data["num_total_crashes"] = search_results["total"]
×
811
            data["signatures"] = search_results["facets"]["signature"]
×
812

813
        logger.debug(
×
814
            "Fetch from Socorro: requesting info for %d signatures",
815
            len(self._signatures),
816
        )
817

818
        data: dict = {}
×
819
        socorro.SuperSearchUnredacted(
×
820
            params=params,
821
            handler=handler,
822
            handlerdata=data,
823
        ).wait()
824

825
        logger.debug(
×
826
            "Fetch from Socorro: received info for %d signatures",
827
            len(data["signatures"]),
828
        )
829

830
        return data["signatures"], data["num_total_crashes"]
×
831

832
    def fetch_bugs(self, include_fields: list[str] = None) -> dict[str, list[dict]]:
×
833
        """Fetch bugs that are filed against the given signatures."""
834
        if not self._signatures:
×
835
            return {}
×
836

837
        params_base: dict = {
×
838
            "include_fields": [
839
                "cf_crash_signature",
840
            ],
841
        }
842

843
        if include_fields:
×
844
            params_base["include_fields"].extend(include_fields)
×
845

846
        params_list = []
×
847
        for signatures_chunk in Connection.chunks(list(self._signatures), 30):
×
848
            params = params_base.copy()
×
849
            n = int(utils.get_last_field_num(params))
×
850
            params[f"f{n}"] = "OP"
×
851
            params[f"j{n}"] = "OR"
×
852
            for signature in signatures_chunk:
×
853
                n += 1
×
854
                params[f"f{n}"] = "cf_crash_signature"
×
855
                params[f"o{n}"] = "regexp"
×
856
                params[f"v{n}"] = rf"\[(@ |@){re.escape(signature)}( \]|\])"
×
857
            params[f"f{n+1}"] = "CP"
×
858
            params_list.append(params)
×
859

860
        signatures_bugs: dict = defaultdict(list)
×
861

862
        def handler(res, data):
×
863
            for bug in res["bugs"]:
×
864
                for signature in utils.get_signatures(bug["cf_crash_signature"]):
×
865
                    if signature in self._signatures:
×
866
                        data[signature].append(bug)
×
867

868
        logger.debug(
×
869
            "Fetch from Bugzilla: requesting bugs for %d signatures",
870
            len(self._signatures),
871
        )
872
        timeout = utils.get_config("common", "bz_query_timeout")
×
873
        Bugzilla(
×
874
            timeout=timeout,
875
            queries=[
876
                connection.Query(Bugzilla.API_URL, params, handler, signatures_bugs)
877
                for params in params_list
878
            ],
879
        ).wait()
880

881
        # TODO: remove the call to DevBugzilla after moving to production
882
        DevBugzilla(
×
883
            timeout=timeout,
884
            queries=[
885
                connection.Query(DevBugzilla.API_URL, params, handler, signatures_bugs)
886
                for params in params_list
887
            ],
888
        ).wait()
889

890
        logger.debug(
×
891
            "Fetch from Bugzilla: received bugs for %d signatures", len(signatures_bugs)
892
        )
893

894
        return signatures_bugs
×
895

896
    def analyze(self) -> list[SignatureAnalyzer]:
×
897
        """Analyze the data related to the signatures."""
898
        bugs = self.fetch_bugs()
×
899
        # TODO(investigate): For now, we are ignoring signatures that have bugs
900
        # filed even if they are closed long time ago. We should investigate
901
        # whether we should include the ones with closed bugs. For example, if
902
        # the bug was closed as Fixed years ago.
903
        self._signatures.difference_update(bugs.keys())
×
904

905
        clouseau_reports = self.fetch_clouseau_crash_reports()
×
906
        # TODO(investigate): For now, we are ignoring signatures that are not
907
        # analyzed by clouseau. We should investigate why they are not analyzed
908
        # and whether we should include them.
909
        self._signatures.intersection_update(clouseau_reports.keys())
×
910

911
        signatures, num_total_crashes = self.fetch_socorro_info()
×
912
        bugs_store = BugsStore()
×
913

914
        return [
×
915
            SignatureAnalyzer(
916
                signature,
917
                num_total_crashes,
918
                clouseau_reports[signature["term"]],
919
                bugs_store,
920
            )
921
            for signature in signatures
922
        ]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc