|
29 | 29 | import io.sentry.Breadcrumb; |
30 | 30 | import io.sentry.Hint; |
31 | 31 | import io.sentry.IpAddressUtils; |
| 32 | +import io.sentry.ProfileChunk; |
| 33 | +import io.sentry.ProfileContext; |
| 34 | +import io.sentry.Sentry; |
32 | 35 | import io.sentry.SentryBaseEvent; |
33 | 36 | import io.sentry.SentryEvent; |
34 | 37 | import io.sentry.SentryExceptionFactory; |
35 | 38 | import io.sentry.SentryLevel; |
36 | 39 | import io.sentry.SentryOptions; |
37 | 40 | import io.sentry.SentryStackTraceFactory; |
38 | 41 | import io.sentry.SpanContext; |
| 42 | +import io.sentry.android.core.anr.AggregatedStackTrace; |
| 43 | +import io.sentry.android.core.anr.AnrCulpritIdentifier; |
| 44 | +import io.sentry.android.core.anr.AnrProfile; |
| 45 | +import io.sentry.android.core.anr.AnrProfileManager; |
| 46 | +import io.sentry.android.core.anr.AnrProfileRotationHelper; |
| 47 | +import io.sentry.android.core.anr.StackTraceConverter; |
39 | 48 | import io.sentry.android.core.internal.util.CpuInfoUtils; |
40 | 49 | import io.sentry.cache.PersistingOptionsObserver; |
41 | 50 | import io.sentry.cache.PersistingScopeObserver; |
| 51 | +import io.sentry.exception.ExceptionMechanismException; |
42 | 52 | import io.sentry.hints.AbnormalExit; |
43 | 53 | import io.sentry.hints.Backfillable; |
44 | 54 | import io.sentry.protocol.App; |
|
50 | 60 | import io.sentry.protocol.OperatingSystem; |
51 | 61 | import io.sentry.protocol.Request; |
52 | 62 | import io.sentry.protocol.SdkVersion; |
| 63 | +import io.sentry.protocol.SentryException; |
| 64 | +import io.sentry.protocol.SentryId; |
| 65 | +import io.sentry.protocol.SentryStackFrame; |
53 | 66 | import io.sentry.protocol.SentryStackTrace; |
54 | 67 | import io.sentry.protocol.SentryThread; |
55 | 68 | import io.sentry.protocol.SentryTransaction; |
56 | 69 | import io.sentry.protocol.User; |
| 70 | +import io.sentry.protocol.profiling.SentryProfile; |
57 | 71 | import io.sentry.util.HintUtils; |
58 | 72 | import io.sentry.util.SentryRandom; |
59 | 73 | import java.io.File; |
@@ -700,16 +714,33 @@ public void applyPreEnrichment( |
700 | 714 | public void applyPostEnrichment( |
701 | 715 | @NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) { |
702 | 716 | final boolean isBackgroundAnr = isBackgroundAnr(rawHint); |
703 | | - setAppForeground(event, !isBackgroundAnr); |
| 717 | + |
| 718 | + if (options.isEnableAnrProfiling()) { |
| 719 | + applyAnrProfile(event, hint, isBackgroundAnr); |
| 720 | + } |
| 721 | + |
704 | 722 | setDefaultAnrFingerprint(event, isBackgroundAnr); |
| 723 | + |
| 724 | + // Set app foreground state |
| 725 | + setAppForeground(event, !isBackgroundAnr); |
705 | 726 | } |
706 | 727 |
|
707 | 728 | private void setDefaultAnrFingerprint( |
708 | 729 | final @NotNull SentryEvent event, final boolean isBackgroundAnr) { |
709 | 730 | // sentry does not yet have a capability to provide default server-side fingerprint rules, |
710 | 731 | // so we're doing this on the SDK side to group background and foreground ANRs separately |
711 | 732 | // even if they have similar stacktraces. |
712 | | - if (event.getFingerprints() == null) { |
| 733 | + if (event.getFingerprints() != null) { |
| 734 | + return; |
| 735 | + } |
| 736 | + |
| 737 | + if (options.isEnableAnrProfiling() && hasOnlySystemFrames(event)) { |
| 738 | + // If profiling did not identify any app frames, we want to statically group these events |
| 739 | + // to avoid ANR noise due to {{ default }} stacktrace grouping |
| 740 | + event.setFingerprints( |
| 741 | + Arrays.asList( |
| 742 | + "system-frames-only-anr", isBackgroundAnr ? "background-anr" : "foreground-anr")); |
| 743 | + } else { |
713 | 744 | event.setFingerprints( |
714 | 745 | Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr")); |
715 | 746 | } |
@@ -777,5 +808,145 @@ private void setAnrExceptions( |
777 | 808 | event.setExceptions( |
778 | 809 | sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr)); |
779 | 810 | } |
| 811 | + |
| 812 | + private void applyAnrProfile( |
| 813 | + @NotNull SentryEvent event, @NotNull Backfillable hint, boolean isBackgroundAnr) { |
| 814 | + |
| 815 | + // Skip background ANRs (as profiling only runs in foreground) |
| 816 | + if (isBackgroundAnr) { |
| 817 | + return; |
| 818 | + } |
| 819 | + |
| 820 | + final String cacheDirPath = options.getCacheDirPath(); |
| 821 | + if (cacheDirPath == null) { |
| 822 | + return; |
| 823 | + } |
| 824 | + final File cacheDir = new File(cacheDirPath); |
| 825 | + |
| 826 | + if (!(hint instanceof AbnormalExit)) { |
| 827 | + return; |
| 828 | + } |
| 829 | + final Long anrTimestampObj = ((AbnormalExit) hint).timestamp(); |
| 830 | + final long anrTimestamp; |
| 831 | + if (anrTimestampObj != null) { |
| 832 | + anrTimestamp = anrTimestampObj; |
| 833 | + } else if (event.getTimestamp() != null) { |
| 834 | + anrTimestamp = event.getTimestamp().getTime(); |
| 835 | + } else { |
| 836 | + return; |
| 837 | + } |
| 838 | + |
| 839 | + AnrProfile anrProfile = null; |
| 840 | + try { |
| 841 | + final File lastFile = AnrProfileRotationHelper.getLastFile(cacheDir); |
| 842 | + if (lastFile.exists()) { |
| 843 | + options.getLogger().log(SentryLevel.DEBUG, "Reading ANR profile"); |
| 844 | + try (final AnrProfileManager provider = new AnrProfileManager(options, lastFile)) { |
| 845 | + anrProfile = provider.load(); |
| 846 | + } |
| 847 | + } else { |
| 848 | + options.getLogger().log(SentryLevel.DEBUG, "No ANR profile file found"); |
| 849 | + } |
| 850 | + } catch (Throwable t) { |
| 851 | + options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile", t); |
| 852 | + } finally { |
| 853 | + if (!AnrProfileRotationHelper.deleteLastFile(cacheDir)) { |
| 854 | + options.getLogger().log(SentryLevel.INFO, "Could not delete ANR profile file"); |
| 855 | + } |
| 856 | + } |
| 857 | + |
| 858 | + if (anrProfile == null) { |
| 859 | + return; |
| 860 | + } |
| 861 | + |
| 862 | + options.getLogger().log(SentryLevel.INFO, "ANR profile found"); |
| 863 | + if (anrTimestamp < anrProfile.startTimeMs || anrTimestamp > anrProfile.endTimeMs) { |
| 864 | + options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match"); |
| 865 | + return; |
| 866 | + } |
| 867 | + |
| 868 | + final AggregatedStackTrace culprit = AnrCulpritIdentifier.identify(anrProfile.stacks); |
| 869 | + if (culprit == null) { |
| 870 | + return; |
| 871 | + } |
| 872 | + |
| 873 | + // Capture profile chunk |
| 874 | + final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile); |
| 875 | + final @NotNull StackTraceElement[] stack = culprit.getStack(); |
| 876 | + |
| 877 | + if (stack.length > 0) { |
| 878 | + final StackTraceElement stackTraceElement = stack[0]; |
| 879 | + final String message = |
| 880 | + stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName(); |
| 881 | + final ApplicationNotResponding exception = new ApplicationNotResponding(message); |
| 882 | + exception.setStackTrace(stack); |
| 883 | + |
| 884 | + final Mechanism mechanism = new Mechanism(); |
| 885 | + mechanism.setType("ANR"); |
| 886 | + final ExceptionMechanismException error = |
| 887 | + new ExceptionMechanismException(mechanism, exception, null, false); |
| 888 | + |
| 889 | + final @NotNull List<SentryException> sentryException = |
| 890 | + sentryExceptionFactory.getSentryExceptions(error); |
| 891 | + |
| 892 | + // Replace the original ANR exception with the profile-derived one, |
| 893 | + // as we assume the profiling culprit identification is more valuable |
| 894 | + // the event threads are kept as-is, so the original main thread stacktrace is still |
| 895 | + // available |
| 896 | + event.setExceptions(sentryException); |
| 897 | + |
| 898 | + if (profilerId != null) { |
| 899 | + event.getContexts().setProfile(new ProfileContext(profilerId)); |
| 900 | + } |
| 901 | + } |
| 902 | + } |
| 903 | + |
| 904 | + @Nullable |
| 905 | + private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfile anrProfile) { |
| 906 | + final SentryProfile profile = StackTraceConverter.convert(anrProfile); |
| 907 | + final ProfileChunk chunk = |
| 908 | + new ProfileChunk( |
| 909 | + new SentryId(), |
| 910 | + new SentryId(), |
| 911 | + null, |
| 912 | + new HashMap<>(0), |
| 913 | + anrTimestampMs / 1000.0d, |
| 914 | + ProfileChunk.PLATFORM_JAVA, |
| 915 | + options); |
| 916 | + chunk.setSentryProfile(profile); |
| 917 | + |
| 918 | + final SentryId profilerId = Sentry.getCurrentScopes().captureProfileChunk(chunk); |
| 919 | + if (SentryId.EMPTY_ID.equals(profilerId)) { |
| 920 | + return null; |
| 921 | + } else { |
| 922 | + return chunk.getProfilerId(); |
| 923 | + } |
| 924 | + } |
| 925 | + |
| 926 | + private boolean hasOnlySystemFrames(@NotNull SentryEvent event) { |
| 927 | + final List<SentryException> exceptions = event.getExceptions(); |
| 928 | + if (exceptions == null || exceptions.isEmpty()) { |
| 929 | + return false; |
| 930 | + } |
| 931 | + |
| 932 | + for (final SentryException exception : exceptions) { |
| 933 | + final SentryStackTrace stacktrace = exception.getStacktrace(); |
| 934 | + if (stacktrace != null) { |
| 935 | + final List<SentryStackFrame> frames = stacktrace.getFrames(); |
| 936 | + if (frames != null && !frames.isEmpty()) { |
| 937 | + for (final SentryStackFrame frame : frames) { |
| 938 | + if (frame.isInApp() != null && frame.isInApp()) { |
| 939 | + return false; |
| 940 | + } |
| 941 | + final String module = frame.getModule(); |
| 942 | + if (module != null && !AnrCulpritIdentifier.isSystemFrame(module)) { |
| 943 | + return false; |
| 944 | + } |
| 945 | + } |
| 946 | + } |
| 947 | + } |
| 948 | + } |
| 949 | + return true; |
| 950 | + } |
780 | 951 | } |
781 | 952 | } |
0 commit comments