Skip to content

Commit 14c6bb1

Browse files
authored
summary: ASP.NET Core 6+ Browser Injectionand Razor Pages Instrumentation
feat: Add automatic browser agent injection for ASP.NET Core v6+ web applications. feat: Add automatic instrumentation for ASP.NET Core v6+ Razor Pages.
1 parent 3ac75a0 commit 14c6bb1

File tree

50 files changed

+1746
-32
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1746
-32
lines changed

FullAgent.sln

+8
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Home", "src\Agent\NewRelic\
192192
{B65A0C00-100D-4F27-BAC7-6B8A9FC7619D} = {B65A0C00-100D-4F27-BAC7-6B8A9FC7619D}
193193
{C51E44B7-ADC9-4EDA-AAAE-F6307180A3EB} = {C51E44B7-ADC9-4EDA-AAAE-F6307180A3EB}
194194
{C60C1767-A73A-4A9E-BAF1-D3463C7CEFEC} = {C60C1767-A73A-4A9E-BAF1-D3463C7CEFEC}
195+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118} = {D4F48A7F-F3D3-4303-921D-BF7FE34B7118}
195196
{D6E22195-EE69-4320-B08B-E68229FB69AB} = {D6E22195-EE69-4320-B08B-E68229FB69AB}
196197
{D9428449-3E4B-4723-A8AA-1191315C7AAD} = {D9428449-3E4B-4723-A8AA-1191315C7AAD}
197198
{E10BF2F9-D5CA-4330-8169-ED30D861697E} = {E10BF2F9-D5CA-4330-8169-ED30D861697E}
@@ -218,6 +219,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elasticsearch", "src\Agent\
218219
EndProject
219220
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kafka", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\Kafka\Kafka.csproj", "{270A9CC8-8031-49F4-A380-1389E7517DB7}"
220221
EndProject
222+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore6Plus", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\AspNetCore6Plus\AspNetCore6Plus.csproj", "{D4F48A7F-F3D3-4303-921D-BF7FE34B7118}"
223+
EndProject
221224
Global
222225
GlobalSection(SolutionConfigurationPlatforms) = preSolution
223226
Debug|Any CPU = Debug|Any CPU
@@ -452,6 +455,10 @@ Global
452455
{270A9CC8-8031-49F4-A380-1389E7517DB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
453456
{270A9CC8-8031-49F4-A380-1389E7517DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
454457
{270A9CC8-8031-49F4-A380-1389E7517DB7}.Release|Any CPU.Build.0 = Release|Any CPU
458+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
459+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118}.Debug|Any CPU.Build.0 = Debug|Any CPU
460+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118}.Release|Any CPU.ActiveCfg = Release|Any CPU
461+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118}.Release|Any CPU.Build.0 = Release|Any CPU
455462
EndGlobalSection
456463
GlobalSection(SolutionProperties) = preSolution
457464
HideSolutionNode = FALSE
@@ -522,6 +529,7 @@ Global
522529
{EC34F023-223D-432F-9401-9C3ED1B75DE4} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
523530
{D9428449-3E4B-4723-A8AA-1191315C7AAD} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
524531
{270A9CC8-8031-49F4-A380-1389E7517DB7} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
532+
{D4F48A7F-F3D3-4303-921D-BF7FE34B7118} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
525533
EndGlobalSection
526534
GlobalSection(ExtensibilityGlobals) = postSolution
527535
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35

build/ArtifactBuilder/CoreAgentComponents.cs

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ protected override void CreateAgentComponents()
5656
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.StackExchangeRedis2Plus.dll",
5757
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.NServiceBus.dll",
5858
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Kafka.dll",
59+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AspNetCore6Plus.dll",
5960
};
6061

6162
var wrapperXmls = new[]
@@ -76,6 +77,7 @@ protected override void CreateAgentComponents()
7677
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.StackExchangeRedis2Plus.Instrumentation.xml",
7778
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.NServiceBus.Instrumentation.xml",
7879
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Kafka.Instrumentation.xml",
80+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AspNetCore6Plus.Instrumentation.xml",
7981
};
8082

8183
ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";

build/build_home.ps1

+9
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ if ($melNetCorePath = Resolve-Path "$wrappersRootDir\MicrosoftExtensionsLogging\
8383
$netFrameworkWrapperHash.Add($dllObject, $xmlObject)
8484
}
8585

86+
# AspNetCore6Plus is built to target .net 6, but we'll copy it to the netstandard folder
87+
if ($aspNetCore6PlusPath = Resolve-Path "$wrappersRootDir\AspNetCore6Plus\bin\$Configuration\net6.0") {
88+
$dllObject = Get-ChildItem -File -Path "$aspNetCore6PlusPath" -Filter NewRelic.Providers.Wrapper.AspNetCore6Plus.dll
89+
$xmlObject = Get-ChildItem -File -Path "$aspNetCore6PlusPath" -Filter Instrumentation.xml
90+
$netstandard20WrapperHash.Add($dllObject, $xmlObject)
91+
}
92+
93+
94+
8695
$netFrameworkStorageArray = @()
8796
$netstandard20StorageArray = @()
8897

src/Agent/MsiInstaller/Installer/Product.wxs

+6
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ SPDX-License-Identifier: Apache-2.0
531531
<Component Id="CoreKafkaWrapperComponent" Guid="{6CA6D5A4-A204-4B04-A6B5-13D8B3E736C2}">
532532
<File Id="CoreKafkaWrapperFile" Name="NewRelic.Providers.Wrapper.Kafka.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Kafka.dll"/>
533533
</Component>
534+
<Component Id="CoreAspNetCore6PlusWrapperComponent" Guid="{3B7C5E60-BA9D-4B1C-8B16-B16025B075C0}">
535+
<File Id="CoreAspNetCore6PlusWrapperFile" Name="NewRelic.Providers.Wrapper.AspNetCore6Plus.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AspNetCore6Plus.dll"/>
536+
</Component>
534537

535538
<!-- Reference libraries -->
536539
<Component Id="CoreNewRelicCoreReferenceComponent" Guid="{DD2BE979-7D4B-47EA-9FBE-F6B381D70E0B}">
@@ -690,6 +693,9 @@ SPDX-License-Identifier: Apache-2.0
690693
<Component Id="CoreKafkaInstrumentationComponent" Guid="{B2C4F83B-A339-4DBD-B8C4-760C8F72F9FC}">
691694
<File Id="CoreKafkaInstrumentationFile" Name="NewRelic.Providers.Wrapper.Kafka.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Kafka.Instrumentation.xml"/>
692695
</Component>
696+
<Component Id="CoreAspNetCore6PlusInstrumentationComponent" Guid="{1CC1E672-5AA5-4F6F-A736-748EA556653A}">
697+
<File Id="CoreAspNetCore6PlusInstrumentationFile" Name="NewRelic.Providers.Wrapper.AspNetCore6Plus.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AspNetCore6Plus.Instrumentation.xml"/>
698+
</Component>
693699
</ComponentGroup>
694700

695701
<!-- Extensions XSD-->

src/Agent/NewRelic/Agent/Core/Agent.cs

+27-8
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
using System.Collections.Generic;
2929
using System.IO;
3030
using System.Linq;
31+
using System.Net.Mime;
32+
using System.Runtime.InteropServices.ComTypes;
3133
using System.Text;
3234
using System.Threading;
35+
using System.Threading.Tasks;
3336

3437
namespace NewRelic.Agent.Core
3538
{
@@ -305,6 +308,28 @@ public Stream TryGetStreamInjector(Stream stream, Encoding encoding, string cont
305308
return null;
306309
}
307310

311+
var script = TryGetRUMScriptInternal(contentType, requestPath);
312+
return script == null ? null : new BrowserMonitoringStreamInjector(() => script, stream, encoding);
313+
}
314+
315+
public async Task TryInjectBrowserScriptAsync(string contentType, string requestPath, byte[] buffer, Stream baseStream)
316+
{
317+
var transaction = _transactionService.GetCurrentInternalTransaction();
318+
319+
var script = TryGetRUMScriptInternal(contentType, requestPath);
320+
var rumBytes = script == null ? null : Encoding.UTF8.GetBytes(script);
321+
322+
if (rumBytes == null)
323+
{
324+
transaction.LogFinest("Skipping RUM Injection: No script was available.");
325+
await baseStream.WriteAsync(buffer, 0, buffer.Length);
326+
}
327+
else
328+
await BrowserScriptInjectionHelper.InjectBrowserScriptAsync(buffer, baseStream, rumBytes, transaction);
329+
}
330+
331+
private string TryGetRUMScriptInternal(string contentType, string requestPath)
332+
{
308333
if (contentType == null)
309334
{
310335
return null;
@@ -332,19 +357,13 @@ public Stream TryGetStreamInjector(Stream stream, Encoding encoding, string cont
332357
// Once the transaction name is used for RUM it must be frozen
333358
transaction.CandidateTransactionName.Freeze(TransactionNameFreezeReason.AutoBrowserScriptInjection);
334359
var script = _browserMonitoringScriptMaker.GetScript(transaction, null);
335-
if (script == null)
336-
{
337-
return null;
338-
}
339360

340-
return new BrowserMonitoringStreamInjector(() => script, stream, encoding);
361+
return script;
341362
}
342363
catch (Exception ex)
343364
{
344365
Log.Error(ex, "RUM: Failed to build Browser Monitoring agent script");
345-
{
346-
return null;
347-
}
366+
return null;
348367
}
349368
}
350369

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using NewRelic.Agent.Api;
8+
using NewRelic.Core.Logging;
9+
10+
namespace NewRelic.Agent.Core.BrowserMonitoring
11+
{
12+
public static class BrowserScriptInjectionHelper
13+
{
14+
/// <summary>
15+
/// Determine where to inject the RUM script and write the buffer to the base stream.
16+
/// </summary>
17+
/// <param name="buffer">UTF-8 encoded buffer representing the current page</param>
18+
/// <param name="baseStream"></param>
19+
/// <param name="rumBytes"></param>
20+
/// <param name="transaction"></param>
21+
/// <returns></returns>
22+
public static async Task InjectBrowserScriptAsync(byte[] buffer, Stream baseStream, byte[] rumBytes, ITransaction transaction)
23+
{
24+
var index = BrowserScriptInjectionIndexHelper.TryFindInjectionIndex(buffer);
25+
26+
if (index == -1)
27+
{
28+
// not found, can't inject anything
29+
transaction?.LogFinest("Skipping RUM Injection: No suitable location found to inject script.");
30+
await baseStream.WriteAsync(buffer, 0, buffer.Length);
31+
return;
32+
}
33+
34+
transaction?.LogFinest($"Injecting RUM script at byte index {index}.");
35+
36+
37+
// Write everything up to the insertion index
38+
await baseStream.WriteAsync(buffer, 0, index);
39+
40+
// Write the RUM script
41+
await baseStream.WriteAsync(rumBytes, 0, rumBytes.Length);
42+
43+
// Write the rest of the doc, starting after the insertion index
44+
await baseStream.WriteAsync(buffer, index, buffer.Length - index);
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
8+
namespace NewRelic.Agent.Core.BrowserMonitoring
9+
{
10+
internal static class BrowserScriptInjectionIndexHelper
11+
{
12+
13+
private static readonly Regex XUaCompatibleFilter = new Regex(@"(<\s*meta[^>]+http-equiv[\s]*=[\s]*['""]x-ua-compatible['""][^>]*>)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);
14+
private static readonly Regex CharsetFilter = new Regex(@"(<\s*meta[^>]+charset\s*=[^>]*>)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);
15+
16+
/// <summary>
17+
/// Returns the index into the (UTF-8 encoded) buffer where the RUM script should be injected, or -1 if no suitable location is found
18+
/// </summary>
19+
/// <param name="content"></param>
20+
/// <returns></returns>
21+
/// <remarks>
22+
/// Specification for Javascript insertion: https://newrelic.atlassian.net/wiki/spaces/eng/pages/50299103/BAM+Agent+Auto-Instrumentation
23+
/// </remarks>
24+
public static int TryFindInjectionIndex(byte[] content)
25+
{
26+
var contentAsString = Encoding.UTF8.GetString(content);
27+
28+
var openingHeadTagIndex = FindFirstOpeningHeadTag(contentAsString);
29+
30+
// No <HEAD> tag. Attempt to insert before <BODY> tag (not a great fallback option).
31+
if (openingHeadTagIndex == -1)
32+
{
33+
return FindIndexBeforeBodyTag(content, contentAsString);
34+
}
35+
36+
// Since we have a head tag (top of 'page'), search for <X_UA_COMPATIBLE> and for <CHARSET> tags in Head section
37+
var xUaCompatibleFilterMatch = XUaCompatibleFilter.Match(contentAsString, openingHeadTagIndex);
38+
var charsetFilterMatch = CharsetFilter.Match(contentAsString, openingHeadTagIndex);
39+
40+
// Try to find </HEAD> tag. (It's okay if we don't find it!)
41+
var closingHeadTagIndex = contentAsString.IndexOf("</head>", StringComparison.InvariantCultureIgnoreCase);
42+
43+
// Find which of the two tags occurs latest (if at all) and ensure that at least
44+
// one of the matches occurs prior to the closing head tag
45+
if ((xUaCompatibleFilterMatch.Success || charsetFilterMatch.Success) &&
46+
(xUaCompatibleFilterMatch.Index < closingHeadTagIndex || charsetFilterMatch.Index < closingHeadTagIndex))
47+
{
48+
var match = charsetFilterMatch;
49+
if (xUaCompatibleFilterMatch.Index > charsetFilterMatch.Index)
50+
{
51+
match = xUaCompatibleFilterMatch;
52+
}
53+
54+
// find the index just after the end of the regex match in the UTF-8 buffer
55+
var contentSubString = contentAsString.Substring(match.Index, match.Length);
56+
var utf8HeadMatchIndex = IndexOfByteArray(content, contentSubString, out var substringBytesLength);
57+
58+
return utf8HeadMatchIndex + substringBytesLength;
59+
}
60+
61+
// found opening head tag but no meta tags, insert immediately after the opening head tag
62+
// Find first '>' after the opening head tag, which will be end of head opening tag.
63+
var indexOfEndHeadOpeningTag = contentAsString.IndexOf('>', openingHeadTagIndex);
64+
65+
// The <HEAD> tag may be malformed or simply be another type of tag, if so do not use it
66+
if (!(indexOfEndHeadOpeningTag > openingHeadTagIndex))
67+
return -1;
68+
69+
// Get the whole open HEAD tag string
70+
var headOpeningTag = contentAsString.Substring(openingHeadTagIndex, (indexOfEndHeadOpeningTag - openingHeadTagIndex) + 1);
71+
var utf8HeadOpeningTagIndex = IndexOfByteArray(content, headOpeningTag, out var headOpeningTagBytesLength);
72+
return utf8HeadOpeningTagIndex + headOpeningTagBytesLength;
73+
}
74+
75+
private static int FindIndexBeforeBodyTag(byte[] content, string contentAsString)
76+
{
77+
const string bodyOpenTag = "<body";
78+
79+
var indexOfBodyTag = contentAsString.IndexOf(bodyOpenTag, StringComparison.InvariantCultureIgnoreCase);
80+
if (indexOfBodyTag < 0)
81+
return -1;
82+
83+
// find the body tag start index in the UTF-8 buffer
84+
var bodyFromContent = contentAsString.Substring(indexOfBodyTag, bodyOpenTag.Length);
85+
var utf8BodyTagIndex = IndexOfByteArray(content, bodyFromContent, out _);
86+
return utf8BodyTagIndex;
87+
}
88+
89+
private static int FindFirstOpeningHeadTag(string content)
90+
{
91+
int indexOpeningHead = -1;
92+
93+
var indexTemp = content.IndexOf("<head", StringComparison.InvariantCultureIgnoreCase);
94+
if (indexTemp < 0)
95+
return -1;
96+
97+
if (content[indexTemp + 5] == '>' || content[indexTemp + 5] == ' ')
98+
{
99+
indexOpeningHead = indexTemp;
100+
}
101+
102+
return indexOpeningHead;
103+
}
104+
105+
/// <summary>
106+
/// Returns an index into a byte array to find a string in the byte array.
107+
/// Exact match using the encoding provided or UTF-8 by default.
108+
/// </summary>
109+
/// <param name="buffer"></param>
110+
/// <param name="stringToFind"></param>
111+
/// <param name="stringToFindBytesLength"></param>
112+
/// <param name="encoding"></param>
113+
/// <returns></returns>
114+
private static int IndexOfByteArray(byte[] buffer, string stringToFind, out int stringToFindBytesLength, Encoding encoding = null)
115+
{
116+
stringToFindBytesLength = 0;
117+
encoding ??= Encoding.UTF8;
118+
119+
if (buffer.Length == 0 || string.IsNullOrEmpty(stringToFind))
120+
return -1;
121+
122+
var stringToFindBytes = encoding.GetBytes(stringToFind);
123+
stringToFindBytesLength = stringToFindBytes.Length;
124+
125+
return buffer.AsSpan().IndexOf(stringToFindBytes);
126+
}
127+
}
128+
}

src/Agent/NewRelic/Agent/Core/Utilities/ExtensionsLoader.cs

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public static void Initialize(string installPathExtensionsDirectory)
3737
{ "GenericHostWebHostBuilderExtensionsWrapper", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore.dll") },
3838
{ "NewRelic.Providers.Wrapper.AspNetCore.InvokeActionMethodAsync", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore.dll") },
3939

40+
{ "BuildCommonServicesWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") },
41+
{ "GenericHostWebHostBuilderExtensionsWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") },
42+
{ "InvokeActionMethodAsyncWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") },
43+
{ "ResponseCompressionBodyOnWriteWrapper", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") },
44+
{ "PageActionInvokeHandlerAsyncWrapper6Plus", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNetCore6Plus.dll") },
45+
4046
{ "ResolveAppWrapper", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.Owin.dll") },
4147

4248
{ "AspNet.CreateEventExecutionStepsTracer", Path.Combine(_installPathExtensionsDirectory, "NewRelic.Providers.Wrapper.AspNet.dll") },

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/IAgent.cs

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Collections.Generic;
1010
using System.IO;
1111
using System.Text;
12+
using System.Threading.Tasks;
1213

1314
namespace NewRelic.Agent.Api
1415
{
@@ -103,6 +104,19 @@ public interface IAgent : IAgentExperimental
103104
/// <param name="requestPath">The path of the request</param>
104105
Stream TryGetStreamInjector(Stream stream, Encoding encoding, string contentType, string requestPath);
105106

107+
/// <summary>
108+
/// Used by AspNetCore6Plus, injects the RUM script if various conditions are met. Assumes (perhaps boldly) that the
109+
/// page content is UTF-8 encoded.
110+
///
111+
/// This method should be called as late as possible (i.e. just before the stream is read) to ensure that the metadata passed in (encoding, contentType, etc) is no longer volatile.
112+
/// </summary>
113+
/// <param name="contentType">The type of content in the stream.</param>
114+
/// <param name="requestPath">The path of the request</param>
115+
/// <param name="buffer">A UTF-8 encoded buffer of the content for this request</param>
116+
/// <param name="baseStream">The stream into which the script (and buffer) should be injected</param>
117+
/// <returns></returns>
118+
Task TryInjectBrowserScriptAsync(string contentType, string requestPath, byte[] buffer, Stream baseStream);
119+
106120
/// <summary>
107121
/// Returns the Trace Metadata of the currently executing transaction.
108122
/// </summary>

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Providers/Wrapper/Constants.cs

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public enum WebTransactionType
3737
ASP,
3838
MVC,
3939
WCF,
40+
Razor,
4041
WebAPI,
4142
WebService,
4243
MonoRail,

0 commit comments

Comments
 (0)