Skip to content

Commit f74223a

Browse files
committed
Revert "Remove error fingerprinting"
This reverts commit f80aa84.
1 parent f80aa84 commit f74223a

3 files changed

Lines changed: 184 additions & 2 deletions

File tree

core/src/main/java/dev/faststats/SimpleErrorTracker.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ public void trackError(final String message, final boolean handled) {
6060
public void trackError(final Throwable error, final boolean handled) {
6161
try {
6262
if (isIgnored(error, Collections.newSetFromMap(new IdentityHashMap<>()))) return;
63-
final var compiled = ErrorHelper.compile(error, null, handled, anonymizationEntries);
64-
final var hashed = MurmurHash3.hash(compiled.toString());
63+
final var hashed = StackTraceFingerprint.hash(error); // todo: report duplicate errors with different messages
6564
if (collected.compute(hashed, (k, v) -> {
6665
return v == null ? 1 : v + 1;
6766
}) > 1) return;
67+
final var compiled = ErrorHelper.compile(error, null, handled, anonymizationEntries);
6868
reports.put(hashed, compiled);
6969
} catch (final NoClassDefFoundError ignored) {
7070
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dev.faststats;
2+
3+
import org.jspecify.annotations.Nullable;
4+
5+
import java.util.Collections;
6+
import java.util.IdentityHashMap;
7+
import java.util.Set;
8+
9+
final class StackTraceFingerprint {
10+
private static final int STACK_TRACE_LIMIT = 5;
11+
12+
private StackTraceFingerprint() {
13+
}
14+
15+
public static String hash(final Throwable error) {
16+
return MurmurHash3.hash(normalize(error));
17+
}
18+
19+
public static String normalize(final Throwable error) {
20+
final var visited = Collections.<Throwable>newSetFromMap(new IdentityHashMap<>());
21+
final var builder = new StringBuilder();
22+
append(error, builder, visited);
23+
return builder.toString();
24+
}
25+
26+
private static void append(@Nullable final Throwable error, final StringBuilder builder, final Set<Throwable> visited) {
27+
if (error == null || !visited.add(error)) return;
28+
29+
if (!builder.isEmpty()) builder.append('\n');
30+
builder.append("e").append(error.getClass().getName());
31+
32+
var frames = 0;
33+
for (final var element : error.getStackTrace()) {
34+
if (ErrorHelper.isLibraryFrame(element.getClassName())) continue;
35+
builder.append("\nf").append(element.getClassName()).append('.').append(element.getMethodName());
36+
if (++frames >= STACK_TRACE_LIMIT) break;
37+
}
38+
39+
append(error.getCause(), builder, visited);
40+
}
41+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package dev.faststats;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
7+
8+
public class StackTraceFingerprintTest {
9+
@Test
10+
public void normalizeIncludesExceptionClassAndFrameOwnersOnly() {
11+
final var error = new RuntimeException("message is ignored");
12+
error.setStackTrace(new StackTraceElement[]{
13+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42),
14+
new StackTraceElement("example.Worker", "call", "Worker.java", 7)
15+
});
16+
17+
assertEquals("""
18+
ejava.lang.RuntimeException
19+
fexample.Plugin.run
20+
fexample.Worker.call""", StackTraceFingerprint.normalize(error));
21+
}
22+
23+
@Test
24+
public void normalizeExcludesLibraryFrames() {
25+
final var error = new RuntimeException("message is ignored");
26+
error.setStackTrace(new StackTraceElement[]{
27+
new StackTraceElement("java.util.ArrayList", "get", "ArrayList.java", 427),
28+
new StackTraceElement("javax.script.ScriptEngine", "eval", "ScriptEngine.java", 1),
29+
new StackTraceElement("sun.reflect.NativeMethodAccessorImpl", "invoke0", "NativeMethodAccessorImpl.java", -2),
30+
new StackTraceElement("com.sun.proxy.Proxy", "invoke", "Proxy.java", 1),
31+
new StackTraceElement("jdk.internal.reflect.DirectMethodHandleAccessor", "invoke", "DirectMethodHandleAccessor.java", 104),
32+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42)
33+
});
34+
35+
assertEquals("""
36+
ejava.lang.RuntimeException
37+
fexample.Plugin.run""", StackTraceFingerprint.normalize(error));
38+
}
39+
40+
@Test
41+
public void normalizeIncludesOnlyFirstFiveNonLibraryFrames() {
42+
final var error = new RuntimeException("message is ignored");
43+
error.setStackTrace(new StackTraceElement[]{
44+
new StackTraceElement("java.util.ArrayList", "get", "ArrayList.java", 427),
45+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42),
46+
new StackTraceElement("example.Worker", "call", "Worker.java", 7),
47+
new StackTraceElement("example.Service", "execute", "Service.java", 15),
48+
new StackTraceElement("example.Repository", "load", "Repository.java", 23),
49+
new StackTraceElement("example.Database", "query", "Database.java", 31),
50+
new StackTraceElement("example.Ignored", "extra", "Ignored.java", 39)
51+
});
52+
53+
assertEquals("""
54+
ejava.lang.RuntimeException
55+
fexample.Plugin.run
56+
fexample.Worker.call
57+
fexample.Service.execute
58+
fexample.Repository.load
59+
fexample.Database.query""", StackTraceFingerprint.normalize(error));
60+
}
61+
62+
@Test
63+
public void normalizeIgnoresMessageFileAndLineNumberDifferences() {
64+
final var first = new RuntimeException("This is error #23f4");
65+
first.setStackTrace(new StackTraceElement[]{
66+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42)
67+
});
68+
final var second = new RuntimeException("This is error #93dsff");
69+
second.setStackTrace(new StackTraceElement[]{
70+
new StackTraceElement("example.Plugin", "run", "Generated.java", 99)
71+
});
72+
73+
assertEquals(StackTraceFingerprint.normalize(first), StackTraceFingerprint.normalize(second));
74+
assertEquals(StackTraceFingerprint.hash(first), StackTraceFingerprint.hash(second));
75+
}
76+
77+
@Test
78+
public void differentExceptionClassChangesFingerprint() {
79+
final var first = new RuntimeException("same");
80+
first.setStackTrace(new StackTraceElement[]{
81+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42)
82+
});
83+
final var second = new IllegalStateException("same");
84+
second.setStackTrace(first.getStackTrace());
85+
86+
assertNotEquals(StackTraceFingerprint.normalize(first), StackTraceFingerprint.normalize(second));
87+
assertNotEquals(StackTraceFingerprint.hash(first), StackTraceFingerprint.hash(second));
88+
}
89+
90+
@Test
91+
public void differentFrameMethodChangesFingerprint() {
92+
final var first = new RuntimeException("same");
93+
first.setStackTrace(new StackTraceElement[]{
94+
new StackTraceElement("example.Plugin", "run", "Plugin.java", 42)
95+
});
96+
final var second = new RuntimeException("same");
97+
second.setStackTrace(new StackTraceElement[]{
98+
new StackTraceElement("example.Plugin", "stop", "Plugin.java", 42)
99+
});
100+
101+
assertNotEquals(StackTraceFingerprint.normalize(first), StackTraceFingerprint.normalize(second));
102+
assertNotEquals(StackTraceFingerprint.hash(first), StackTraceFingerprint.hash(second));
103+
}
104+
105+
@Test
106+
public void normalizeIncludesNestedCausesInOrder() {
107+
final var root = new IllegalArgumentException("root");
108+
root.setStackTrace(new StackTraceElement[]{
109+
new StackTraceElement("example.Root", "fail", "Root.java", 10)
110+
});
111+
final var top = new RuntimeException("top", root);
112+
top.setStackTrace(new StackTraceElement[]{
113+
new StackTraceElement("example.Top", "run", "Top.java", 30)
114+
});
115+
116+
assertEquals("""
117+
ejava.lang.RuntimeException
118+
fexample.Top.run
119+
ejava.lang.IllegalArgumentException
120+
fexample.Root.fail""", StackTraceFingerprint.normalize(top));
121+
}
122+
123+
@Test
124+
public void cyclicCauseChainStopsAfterFirstVisit() {
125+
final var first = new RuntimeException("first");
126+
first.setStackTrace(new StackTraceElement[]{
127+
new StackTraceElement("example.First", "run", "First.java", 1)
128+
});
129+
final var second = new IllegalStateException("second", first);
130+
second.setStackTrace(new StackTraceElement[]{
131+
new StackTraceElement("example.Second", "call", "Second.java", 2)
132+
});
133+
first.initCause(second);
134+
135+
assertEquals("""
136+
ejava.lang.RuntimeException
137+
fexample.First.run
138+
ejava.lang.IllegalStateException
139+
fexample.Second.call""", StackTraceFingerprint.normalize(first));
140+
}
141+
}

0 commit comments

Comments
 (0)