-
Notifications
You must be signed in to change notification settings - Fork 320
3143 add support for vertx web 4 #3557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
devinsba
merged 15 commits into
DataDog:master
from
LuboViluda:3143-addSupportFor-vertx-web-4
Jun 14, 2022
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
c587ead
3443 - add instrumentation support for Vertx web 4.0 and above
a7b22e7
3443 - add instrumentation support for Vertx web 4.0 and above (build…
fec2180
3443 - reformat code
164732b
3443 - fix tests
c832c1f
3443 - test improvements
ffa072a
3443 - use 0 port
5ea0cd2
3443 - change versioning to reflect minimal supported version (4.0.0)
be42ee9
3443 - deduplicate backslash when concatenating router path and path
3d1c4d5
Update dd-java-agent/instrumentation/vertx-web-4.0/src/test/groovy/se…
LuboViluda 43df101
3443 - apply suggestion - RoutingContextImplInstrumentation adviceTra…
bc86958
3443 - apply suggestion - RoutingContextImplInstrumentation adviceTra…
ce79aa0
3443 - fix build missing import (:
262c765
3443 - RouteHandlerWrapperAdvice wrap hander on RouteImpl instead of …
38c8b7b
3443 - remove replaceAll usage (fix static analysis check)
662b63f
Add muzzle check to other instrumentation classes
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
...g/trace/instrumentation/vertx_4_0/server/HttpServerResponseEndHandlerInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isPublic; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.muzzle.Reference; | ||
|
|
||
| @AutoService(Instrumenter.class) | ||
| public class HttpServerResponseEndHandlerInstrumentation extends Instrumenter.Tracing | ||
| implements Instrumenter.ForKnownTypes { | ||
| public HttpServerResponseEndHandlerInstrumentation() { | ||
| super("vertx", "vertx-4.0"); | ||
| } | ||
|
|
||
| @Override | ||
| public Reference[] additionalMuzzleReferences() { | ||
| return new Reference[] {VertxVersionMatcher.HTTP_1X_SERVER_RESPONSE}; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] helperClassNames() { | ||
| return new String[] { | ||
| packageName + ".EndHandlerWrapper", | ||
| packageName + ".RouteHandlerWrapper", | ||
| packageName + ".VertxDecorator", | ||
| packageName + ".VertxDecorator$VertxURIDataAdapter", | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] knownMatchingTypes() { | ||
| return new String[] { | ||
| "io.vertx.core.http.impl.Http1xServerResponse", "io.vertx.core.http.impl.Http2ServerResponse " | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public void adviceTransformations(AdviceTransformation transformation) { | ||
| transformation.applyAdvice( | ||
| isMethod() | ||
| .and(named("endHandler")) | ||
| .and(isPublic()) | ||
| .and(takesArgument(0, named("io.vertx.core.Handler"))), | ||
| packageName + ".EndHandlerWrapperAdvice"); | ||
| } | ||
| } |
48 changes: 48 additions & 0 deletions
48
...main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isPublic; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.muzzle.Reference; | ||
|
|
||
| @AutoService(Instrumenter.class) | ||
| public class RouteHandlerInstrumentation extends Instrumenter.Tracing | ||
| implements Instrumenter.ForSingleType { | ||
| public RouteHandlerInstrumentation() { | ||
| super("vertx", "vertx-4.0"); | ||
| } | ||
|
|
||
| @Override | ||
| public Reference[] additionalMuzzleReferences() { | ||
| return new Reference[] {VertxVersionMatcher.HTTP_1X_SERVER_RESPONSE}; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] helperClassNames() { | ||
| return new String[] { | ||
| packageName + ".EndHandlerWrapper", | ||
| packageName + ".RouteHandlerWrapper", | ||
| packageName + ".VertxDecorator", | ||
| packageName + ".VertxDecorator$VertxURIDataAdapter", | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public String instrumentedType() { | ||
| return "io.vertx.ext.web.impl.RouteImpl"; | ||
| } | ||
|
|
||
| @Override | ||
| public void adviceTransformations(AdviceTransformation transformation) { | ||
| transformation.applyAdvice( | ||
| isMethod() | ||
| .and(named("handler")) | ||
| .and(isPublic()) | ||
| .and(takesArgument(0, named("io.vertx.core.Handler"))), | ||
| packageName + ".RouteHandlerWrapperAdvice"); | ||
| } | ||
| } |
65 changes: 65 additions & 0 deletions
65
...rc/main/java/datadog/trace/instrumentation/vertx_4_0/server/RouteImplInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isPublic; | ||
| import static net.bytebuddy.matcher.ElementMatchers.returns; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.muzzle.Reference; | ||
|
|
||
| @AutoService(Instrumenter.class) | ||
| public class RouteImplInstrumentation extends Instrumenter.AppSec | ||
| implements Instrumenter.ForKnownTypes { | ||
|
|
||
| public RouteImplInstrumentation() { | ||
| super("vertx", "vertx-4.0"); | ||
| } | ||
|
|
||
| @Override | ||
| public Reference[] additionalMuzzleReferences() { | ||
| return new Reference[] {VertxVersionMatcher.HTTP_1X_SERVER_RESPONSE}; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] knownMatchingTypes() { | ||
| return new String[] { | ||
| "io.vertx.ext.web.impl.RouteImpl", "io.vertx.ext.web.impl.RouteState", | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] helperClassNames() { | ||
| return new String[] { | ||
| packageName + ".PathParameterPublishingHelper", | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public void adviceTransformations(AdviceTransformation transformation) { | ||
| transformation.applyAdvice( | ||
| named("matches") | ||
| .and(takesArguments(3)) | ||
| .and(takesArgument(0, named("io.vertx.ext.web.impl.RoutingContextImplBase"))) | ||
| .and(takesArgument(1, String.class)) | ||
| .and(takesArgument(2, boolean.class)) | ||
| .and(isPublic()) | ||
| .and(returns(int.class)), | ||
| packageName + ".RouteMatchesAdvice"); | ||
|
|
||
| transformation.applyAdvice( | ||
| named("matches") | ||
| .and(takesArguments(3)) | ||
| .and( | ||
| takesArgument( | ||
| 0, | ||
| named("io.vertx.ext.web.impl.RoutingContextImplBase") | ||
| .or(named("io.vertx.ext.web.RoutingContext")))) | ||
| .and(takesArgument(1, String.class)) | ||
| .and(takesArgument(2, boolean.class)) | ||
| .and(returns(boolean.class)), | ||
| packageName + ".RouteMatchesAdvice$BooleanReturnVariant"); | ||
| } | ||
| } | ||
38 changes: 38 additions & 0 deletions
38
...ava/datadog/trace/instrumentation/vertx_4_0/server/RoutingContextImplInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.muzzle.Reference; | ||
|
|
||
| @AutoService(Instrumenter.class) | ||
| public class RoutingContextImplInstrumentation extends Instrumenter.AppSec | ||
| implements Instrumenter.ForSingleType { | ||
|
|
||
| public RoutingContextImplInstrumentation() { | ||
| super("vertx", "vertx-4.0"); | ||
| } | ||
|
|
||
| @Override | ||
| public Reference[] additionalMuzzleReferences() { | ||
| return new Reference[] {VertxVersionMatcher.HTTP_1X_SERVER_RESPONSE}; | ||
| } | ||
|
|
||
| @Override | ||
| public String instrumentedType() { | ||
| return "io.vertx.ext.web.impl.RoutingContextImpl"; | ||
| } | ||
mcculls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Override | ||
| public void adviceTransformations(AdviceTransformation transformation) { | ||
| transformation.applyAdvice( | ||
| named("getBodyAsJson") | ||
| .or(named("getBodyAsJsonArray")) | ||
| .and(takesArguments(1)) | ||
| .and(takesArgument(0, int.class)), | ||
| packageName + ".RoutingContextJsonAdvice"); | ||
| } | ||
bantonsson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
10 changes: 10 additions & 0 deletions
10
...4.0/src/main/java/datadog/trace/instrumentation/vertx_4_0/server/VertxVersionMatcher.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import datadog.trace.agent.tooling.muzzle.Reference; | ||
|
|
||
| // checks for vertx > 4 | ||
| public class VertxVersionMatcher { | ||
mcculls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // added in 4.0 | ||
| static final Reference HTTP_1X_SERVER_RESPONSE = | ||
| new Reference.Builder("io.vertx.core.http.impl.Http1xServerResponse").build(); | ||
| } | ||
41 changes: 41 additions & 0 deletions
41
...-4.0/src/main/java8/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.HANDLER_SPAN_CONTEXT_KEY; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.PARENT_SPAN_CONTEXT_KEY; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.RouteHandlerWrapper.ROUTE_CONTEXT_KEY; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE; | ||
|
|
||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import io.vertx.core.Handler; | ||
| import io.vertx.ext.web.RoutingContext; | ||
|
|
||
| public class EndHandlerWrapper implements Handler<Void> { | ||
| private final RoutingContext routingContext; | ||
|
|
||
| Handler<Void> actual; | ||
|
|
||
| EndHandlerWrapper(RoutingContext routingContext) { | ||
| this.routingContext = routingContext; | ||
| } | ||
|
|
||
| @Override | ||
| public void handle(final Void event) { | ||
| AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); | ||
| AgentSpan parentSpan = routingContext.get(PARENT_SPAN_CONTEXT_KEY); | ||
| String path = routingContext.get(ROUTE_CONTEXT_KEY); | ||
| try { | ||
| span.finishThreadMigration(); | ||
| if (actual != null) { | ||
| actual.handle(event); | ||
| } | ||
| } finally { | ||
| if (path != null) { | ||
| HTTP_RESOURCE_DECORATOR.withRoute( | ||
| parentSpan, routingContext.request().method().name(), path, true); | ||
| } | ||
| DECORATE.onResponse(span, routingContext.response()); | ||
| span.finish(); | ||
| } | ||
| } | ||
| } |
27 changes: 27 additions & 0 deletions
27
...rc/main/java8/datadog/trace/instrumentation/vertx_4_0/server/EndHandlerWrapperAdvice.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import io.vertx.core.Handler; | ||
| import net.bytebuddy.asm.Advice; | ||
|
|
||
| public class EndHandlerWrapperAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void wrapHandler( | ||
| @Advice.FieldValue(value = "endHandler", readOnly = false) final Handler<Void> endHandler, | ||
| @Advice.Argument(value = 0, readOnly = false) Handler<Void> handler) { | ||
| // In case the handler instrumentation executes twice on the same response | ||
| if (endHandler instanceof EndHandlerWrapper && handler instanceof EndHandlerWrapper) { | ||
| return; | ||
| } | ||
| // If an end handler was already registered when our wrapper is registered, we save the one that | ||
| // existed before | ||
| if (handler instanceof EndHandlerWrapper && endHandler != null) { | ||
| ((EndHandlerWrapper) handler).actual = endHandler; | ||
|
|
||
| // If the user registers an end handler and ours has already been registered then we wrap the | ||
| // users handler and swap the function argument for the wrapper | ||
| } else if (endHandler instanceof EndHandlerWrapper) { | ||
| ((EndHandlerWrapper) endHandler).actual = handler; | ||
| handler = endHandler; | ||
| } | ||
| } | ||
| } |
31 changes: 31 additions & 0 deletions
31
...n/java8/datadog/trace/instrumentation/vertx_4_0/server/PathParameterPublishingHelper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.api.gateway.Events.EVENTS; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; | ||
|
|
||
| import datadog.trace.api.function.BiFunction; | ||
| import datadog.trace.api.gateway.CallbackProvider; | ||
| import datadog.trace.api.gateway.Flow; | ||
| import datadog.trace.api.gateway.RequestContext; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentTracer; | ||
| import java.util.Map; | ||
|
|
||
| public class PathParameterPublishingHelper { | ||
| public static void publishParams(Map<String, String> params) { | ||
| AgentSpan agentSpan = activeSpan(); | ||
| if (agentSpan == null) { | ||
| return; | ||
| } | ||
|
|
||
| CallbackProvider cbp = AgentTracer.get().instrumentationGateway(); | ||
| BiFunction<RequestContext<Object>, Map<String, ?>, Flow<Void>> callback = | ||
| cbp.getCallback(EVENTS.requestPathParams()); | ||
| RequestContext<Object> requestContext = agentSpan.getRequestContext(); | ||
| if (requestContext == null || callback == null) { | ||
| return; | ||
| } | ||
|
|
||
| callback.apply(requestContext, params); | ||
| } | ||
| } |
79 changes: 79 additions & 0 deletions
79
....0/src/main/java8/datadog/trace/instrumentation/vertx_4_0/server/RouteHandlerWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package datadog.trace.instrumentation.vertx_4_0.server; | ||
|
|
||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.DECORATE; | ||
| import static datadog.trace.instrumentation.vertx_4_0.server.VertxDecorator.INSTRUMENTATION_NAME; | ||
|
|
||
| import datadog.trace.bootstrap.instrumentation.api.AgentScope; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.bootstrap.instrumentation.api.Tags; | ||
| import io.vertx.core.Handler; | ||
| import io.vertx.ext.web.RoutingContext; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class RouteHandlerWrapper implements Handler<RoutingContext> { | ||
| private static final Logger log = LoggerFactory.getLogger(RouteHandlerWrapper.class); | ||
| static final String PARENT_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".parent"; | ||
| static final String HANDLER_SPAN_CONTEXT_KEY = AgentSpan.class.getName() + ".handler"; | ||
| static final String ROUTE_CONTEXT_KEY = "dd." + Tags.HTTP_ROUTE; | ||
|
|
||
| private final Handler<RoutingContext> actual; | ||
|
|
||
| public RouteHandlerWrapper(final Handler<RoutingContext> handler) { | ||
| actual = handler; | ||
| } | ||
|
|
||
| @Override | ||
| public void handle(final RoutingContext routingContext) { | ||
| AgentSpan span = routingContext.get(HANDLER_SPAN_CONTEXT_KEY); | ||
| if (span == null) { | ||
| AgentSpan parentSpan = activeSpan(); | ||
| routingContext.put(PARENT_SPAN_CONTEXT_KEY, parentSpan); | ||
|
|
||
| span = startSpan(INSTRUMENTATION_NAME); | ||
| routingContext.put(HANDLER_SPAN_CONTEXT_KEY, span); | ||
| // span is stored in the context and the span related work may proceed on any thread | ||
| span.startThreadMigration(); | ||
|
|
||
| routingContext.response().endHandler(new EndHandlerWrapper(routingContext)); | ||
| DECORATE.afterStart(span); | ||
| span.setResourceName(DECORATE.className(actual.getClass())); | ||
| } else { | ||
| // the span was retrieved from the context in 'suspended' state - need to be resumed | ||
| span.finishThreadMigration(); | ||
| } | ||
|
|
||
| updateRoutingContextWithRoute(routingContext); | ||
|
|
||
| try (final AgentScope scope = activateSpan(span)) { | ||
| scope.setAsyncPropagation(true); | ||
| try { | ||
| actual.handle(routingContext); | ||
| } catch (final Throwable t) { | ||
| DECORATE.onError(span, t); | ||
| throw t; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void updateRoutingContextWithRoute(RoutingContext routingContext) { | ||
| final String method = routingContext.request().method().name(); | ||
| final String mountPoint = routingContext.mountPoint(); | ||
|
|
||
| String path = routingContext.currentRoute().getPath(); | ||
|
|
||
| if (mountPoint != null) { | ||
| final String noBackslashhMountPoint = | ||
| mountPoint.endsWith("/") | ||
| ? mountPoint.substring(0, mountPoint.lastIndexOf("/")) | ||
| : mountPoint; | ||
| path = noBackslashhMountPoint + path; | ||
| } | ||
| if (method != null && path != null) { | ||
| routingContext.put(ROUTE_CONTEXT_KEY, path); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.