Skip to content

Commit 96d1d4a

Browse files
authored
Merge cacc249 into f6cdbf0
2 parents f6cdbf0 + cacc249 commit 96d1d4a

3 files changed

Lines changed: 143 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Improvements
6+
7+
- Improve SDK init performance by replacing `java.net.URI` with custom string parsing for DSN ([#5448](https://github.com/getsentry/sentry-java/pull/5448))
8+
59
### Features
610

711
- Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387))

sentry/src/main/java/io/sentry/Dsn.java

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,54 +53,100 @@ URI getSentryUri() {
5353
return sentryUri;
5454
}
5555

56+
// Avoids java.net.URI for DSN parsing, which is slow on Android.
5657
Dsn(@Nullable String dsn) throws IllegalArgumentException {
5758
try {
5859
final String dsnString = Objects.requireNonNull(dsn, "The DSN is required.").trim();
5960
if (dsnString.isEmpty()) {
6061
throw new IllegalArgumentException("The DSN is empty.");
6162
}
62-
final URI uri = new URI(dsnString).normalize();
63-
final String scheme = uri.getScheme();
63+
64+
// Extract scheme
65+
final int schemeEnd = dsnString.indexOf("://");
66+
if (schemeEnd < 0) {
67+
throw new IllegalArgumentException("Invalid DSN: missing scheme.");
68+
}
69+
final String scheme = dsnString.substring(0, schemeEnd);
6470
if (!("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) {
6571
throw new IllegalArgumentException("Invalid DSN scheme: " + scheme);
6672
}
6773

68-
String userInfo = uri.getUserInfo();
69-
if (userInfo == null || userInfo.isEmpty()) {
74+
// Extract userinfo (public key and optional secret key)
75+
final int authStart = schemeEnd + 3;
76+
final int atIndex = dsnString.indexOf('@', authStart);
77+
if (atIndex < 0) {
78+
throw new IllegalArgumentException("Invalid DSN: No public key provided.");
79+
}
80+
final String userInfo = dsnString.substring(authStart, atIndex);
81+
if (userInfo.isEmpty()) {
7082
throw new IllegalArgumentException("Invalid DSN: No public key provided.");
7183
}
72-
String[] keys = userInfo.split(":", -1);
73-
publicKey = keys[0];
74-
if (publicKey == null || publicKey.isEmpty()) {
84+
final int colonIndex = userInfo.indexOf(':');
85+
if (colonIndex < 0) {
86+
publicKey = userInfo;
87+
secretKey = null;
88+
} else {
89+
publicKey = userInfo.substring(0, colonIndex);
90+
secretKey = userInfo.substring(colonIndex + 1);
91+
}
92+
if (publicKey.isEmpty()) {
7593
throw new IllegalArgumentException("Invalid DSN: No public key provided.");
7694
}
77-
secretKey = keys.length > 1 ? keys[1] : null;
78-
String uriPath = uri.getPath();
79-
if (uriPath.endsWith("/")) {
80-
uriPath = uriPath.substring(0, uriPath.length() - 1);
95+
96+
// Extract host, optional port, and path+projectId
97+
final int hostStart = atIndex + 1;
98+
99+
// Strip query string if present
100+
final int queryIndex = dsnString.indexOf('?', hostStart);
101+
final String hostAndPath =
102+
queryIndex < 0
103+
? dsnString.substring(hostStart)
104+
: dsnString.substring(hostStart, queryIndex);
105+
106+
final int firstSlash = hostAndPath.indexOf('/');
107+
if (firstSlash < 0) {
108+
throw new IllegalArgumentException("Invalid DSN: A Project Id is required.");
109+
}
110+
111+
final String hostPort = hostAndPath.substring(0, firstSlash);
112+
final int portColon = hostPort.indexOf(':');
113+
final String host;
114+
final int port;
115+
if (portColon < 0) {
116+
host = hostPort;
117+
port = -1;
118+
} else {
119+
host = hostPort.substring(0, portColon);
120+
port = Integer.parseInt(hostPort.substring(portColon + 1));
121+
}
122+
123+
// Normalize the path (collapse double slashes, like URI.normalize())
124+
String rawPath = hostAndPath.substring(firstSlash);
125+
while (rawPath.contains("//")) {
126+
rawPath = rawPath.replace("//", "/");
127+
}
128+
129+
if (rawPath.endsWith("/")) {
130+
rawPath = rawPath.substring(0, rawPath.length() - 1);
81131
}
82-
int projectIdStart = uriPath.lastIndexOf("/") + 1;
83-
String path = uriPath.substring(0, projectIdStart);
84-
if (!path.endsWith("/")) {
85-
path += "/";
132+
final int projectIdStart = rawPath.lastIndexOf('/') + 1;
133+
String pathSegment = rawPath.substring(0, projectIdStart);
134+
if (!pathSegment.endsWith("/")) {
135+
pathSegment += "/";
86136
}
87-
this.path = path;
88-
projectId = uriPath.substring(projectIdStart);
137+
this.path = pathSegment;
138+
projectId = rawPath.substring(projectIdStart);
89139
if (projectId.isEmpty()) {
90140
throw new IllegalArgumentException("Invalid DSN: A Project Id is required.");
91141
}
92-
sentryUri =
93-
new URI(
94-
scheme, null, uri.getHost(), uri.getPort(), path + "api/" + projectId, null, null);
142+
143+
sentryUri = new URI(scheme, null, host, port, pathSegment + "api/" + projectId, null, null);
95144

96145
// Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123")
97146
String extractedOrgId = null;
98-
final String host = uri.getHost();
99-
if (host != null) {
100-
final Matcher matcher = ORG_ID_PATTERN.matcher(host);
101-
if (matcher.find()) {
102-
extractedOrgId = matcher.group(1);
103-
}
147+
final Matcher matcher = ORG_ID_PATTERN.matcher(host);
148+
if (matcher.find()) {
149+
extractedOrgId = matcher.group(1);
104150
}
105151
orgId = extractedOrgId;
106152
} catch (Throwable e) {

sentry/src/test/java/io/sentry/DsnTest.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,71 @@ class DsnTest {
145145
val dsn = Dsn("http://key@localhost:9000/456")
146146
assertNull(dsn.orgId)
147147
}
148+
149+
@Test
150+
fun `when dsn is null, throws exception`() {
151+
assertFailsWith<IllegalArgumentException> { Dsn(null) }
152+
}
153+
154+
@Test
155+
fun `when dsn has no scheme separator, throws exception`() {
156+
assertFailsWith<IllegalArgumentException> { Dsn("httpspublicKey@host/id") }
157+
}
158+
159+
@Test
160+
fun `when dsn has no slash after host, throws exception`() {
161+
assertFailsWith<IllegalArgumentException> { Dsn("https://key@host") }
162+
}
163+
164+
@Test
165+
fun `dsn parsed with multiple path segments`() {
166+
val dsn = Dsn("https://key@host/path/to/sentry/id")
167+
168+
assertEquals("https://host/path/to/sentry/api/id", dsn.sentryUri.toURL().toString())
169+
assertEquals("key", dsn.publicKey)
170+
assertEquals("/path/to/sentry/", dsn.path)
171+
assertEquals("id", dsn.projectId)
172+
}
173+
174+
@Test
175+
fun `dsn parsed with port and path`() {
176+
val dsn = Dsn("http://key:secret@host:8080/path/id")
177+
178+
assertEquals("http://host:8080/path/api/id", dsn.sentryUri.toURL().toString())
179+
assertEquals("key", dsn.publicKey)
180+
assertEquals("secret", dsn.secretKey)
181+
assertEquals("/path/", dsn.path)
182+
assertEquals("id", dsn.projectId)
183+
}
184+
185+
@Test
186+
fun `dsn with multiple double slashes in path is normalized`() {
187+
val dsn = Dsn("http://key@host//path//id")
188+
assertEquals("http://host/path/api/id", dsn.sentryUri.toURL().toString())
189+
}
190+
191+
@Test
192+
fun `dsn with query string and port`() {
193+
val dsn = Dsn("https://key@host:443/id?foo=bar&baz=1")
194+
195+
assertEquals("https://host:443/api/id", dsn.sentryUri.toURL().toString())
196+
assertEquals("id", dsn.projectId)
197+
}
198+
199+
@Test
200+
fun `dsn with empty secret key after colon`() {
201+
val dsn = Dsn("https://publicKey:@host/id")
202+
203+
assertEquals("publicKey", dsn.publicKey)
204+
assertEquals("", dsn.secretKey)
205+
}
206+
207+
@Test
208+
fun `dsn with numeric project id`() {
209+
val dsn = Dsn("https://[email protected]/1234567")
210+
211+
assertEquals("1234567", dsn.projectId)
212+
assertEquals("123", dsn.orgId)
213+
assertEquals("https://o123.ingest.sentry.io/api/1234567", dsn.sentryUri.toURL().toString())
214+
}
148215
}

0 commit comments

Comments
 (0)