diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c684ed27..d2857a1d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,6 +197,18 @@ jobs: path: samples/artifacts/builds/Android if-no-files-found: error + - name: Build WebGL Player + run: | + docker exec unity dotnet msbuild /t:UnityConfigureSentryOptions /p:TestDsn=http://publickey@127.0.0.1:8000/12345 /p:Configuration=Release /p:OutDir=other src/Sentry.Unity + docker exec unity dotnet msbuild /t:UnityBuildPlayerWebGL /p:Configuration=Release /p:OutDir=other src/Sentry.Unity + + - name: Upload WebGL Build + uses: actions/upload-artifact@v2 + with: + name: testapp-webgl-${{ matrix.unity-version }} + path: samples/artifacts/builds/WebGL + if-no-files-found: error + package-validation: needs: [build] name: UPM Package validation @@ -453,3 +465,26 @@ jobs: $runtime = "iOS " + $runtime } ./Scripts/smoke-test-ios.ps1 Test "$runtime" + + webgl-smoke-test: + needs: [build] + name: Run WebGL Unity ${{ matrix.unity-version }} Smoke Test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + unity-version: ['2019', '2020', '2021'] + steps: + - name: Checkout + uses: actions/checkout@v2.3.3 + + - name: Download test app artifact + uses: actions/download-artifact@v2 + with: + name: testapp-webgl-${{ matrix.unity-version }} + path: samples/artifacts/builds/WebGL + + - run: pip3 install --upgrade --user selenium urllib3 requests + + - run: python3 scripts/smoke-test-webgl.py + timeout-minutes: 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3a334d6..ab309432e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- WebGL - .NET support ([#657](https://github.com/getsentry/sentry-unity/pull/657)) - Capture `Debug.LogError()` and `Debug.LogException()` also on background threads ([#673](https://github.com/getsentry/sentry-unity/pull/673)) - Adding override for Sentry CLI URL ([#666](https://github.com/getsentry/sentry-unity/pull/666)) diff --git a/Directory.Build.targets b/Directory.Build.targets index 78f7834c3..b1619e211 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -254,6 +254,20 @@ Related: https://forum.unity.com/threads/6572-debugger-agent-unable-to-listen-on + + + + + + + + + + + + + + diff --git a/package-dev/Runtime/SentryInitialization.cs b/package-dev/Runtime/SentryInitialization.cs index 75fac4de0..2c6381e62 100644 --- a/package-dev/Runtime/SentryInitialization.cs +++ b/package-dev/Runtime/SentryInitialization.cs @@ -5,6 +5,8 @@ #define SENTRY_NATIVE_ANDROID #elif UNITY_STANDALONE_WIN && ENABLE_IL2CPP #define SENTRY_NATIVE_WINDOWS +#elif UNITY_WEBGL +#define SENTRY_WEBGL #endif #endif @@ -17,6 +19,8 @@ using Sentry.Unity.Android; #elif SENTRY_NATIVE_WINDOWS using Sentry.Unity.Native; +#elif SENTRY_WEBGL +using Sentry.Unity.WebGL; #endif [assembly: AlwaysLinkAssembly] @@ -39,6 +43,8 @@ public static void Init() SentryNativeAndroid.Configure(options, sentryUnityInfo); #elif SENTRY_NATIVE_WINDOWS SentryNative.Configure(options); +#elif SENTRY_WEBGL + SentryWebGL.Configure(options); #endif SentryUnity.Init(options); diff --git a/samples/unity-of-bugs/Assets/Editor/Builder.cs b/samples/unity-of-bugs/Assets/Editor/Builder.cs index 748d503d4..b8641900d 100644 --- a/samples/unity-of-bugs/Assets/Editor/Builder.cs +++ b/samples/unity-of-bugs/Assets/Editor/Builder.cs @@ -67,6 +67,7 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group) public static void BuildLinuxIl2CPPPlayer() => BuildIl2CPPPlayer(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone); public static void BuildAndroidIl2CPPPlayer() => BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android); public static void BuildIOSPlayer() => BuildIl2CPPPlayer(BuildTarget.iOS, BuildTargetGroup.iOS); + public static void BuildWebGLPlayer() => BuildIl2CPPPlayer(BuildTarget.WebGL, BuildTargetGroup.WebGL); private static void SetupSentryOptions(Dictionary args) { diff --git a/samples/unity-of-bugs/Assets/Scenes/2_NativeSupport.unity b/samples/unity-of-bugs/Assets/Scenes/2_NativeSupport.unity index 9423960c6..09a9849ad 100644 --- a/samples/unity-of-bugs/Assets/Scenes/2_NativeSupport.unity +++ b/samples/unity-of-bugs/Assets/Scenes/2_NativeSupport.unity @@ -887,6 +887,83 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 560000143} m_CullTransparentMesh: 0 +--- !u!1 &565393628 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 565393629} + - component: {fileID: 565393630} + - component: {fileID: 565393631} + m_Layer: 5 + m_Name: WebGL + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &565393629 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 565393628} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1519446888} + - {fileID: 1334676550} + - {fileID: 1313662201} + m_Father: {fileID: 1665572489} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -220} + m_SizeDelta: {x: 0, y: 90} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &565393630 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 565393628} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 088c449aca9f79c4b929eea17a498d7d, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &565393631 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 565393628} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 1 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 --- !u!1 &567661510 GameObject: m_ObjectHideFlags: 0 @@ -1811,6 +1888,214 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1180987020} m_CullTransparentMesh: 0 +--- !u!1 &1313662200 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1313662201} + - component: {fileID: 1313662204} + - component: {fileID: 1313662203} + - component: {fileID: 1313662202} + m_Layer: 5 + m_Name: ThrowJS + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1313662201 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313662200} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2070520013} + m_Father: {fileID: 565393629} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1313662202 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313662200} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1313662203} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 565393630} + m_MethodName: ThrowJavaScript + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 +--- !u!114 &1313662203 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313662200} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1313662204 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313662200} + m_CullTransparentMesh: 0 +--- !u!1 &1334676549 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1334676550} + - component: {fileID: 1334676552} + - component: {fileID: 1334676551} + m_Layer: 5 + m_Name: WebGL + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1334676550 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334676549} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 565393629} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &1334676551 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334676549} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: JavaScript (WebGL) +--- !u!222 &1334676552 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334676549} + m_CullTransparentMesh: 0 --- !u!1 &1348565635 GameObject: m_ObjectHideFlags: 0 @@ -2065,6 +2350,41 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1455265211} m_CullTransparentMesh: 0 +--- !u!1 &1519446887 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1519446888} + m_Layer: 5 + m_Name: =================== + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1519446888 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1519446887} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 565393629} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 5} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &1574187300 GameObject: m_ObjectHideFlags: 0 @@ -2135,6 +2455,7 @@ RectTransform: - {fileID: 253040315} - {fileID: 1167211827} - {fileID: 121492395} + - {fileID: 565393629} - {fileID: 2041051215464099389} m_Father: {fileID: 0} m_RootOrder: 2 @@ -2218,6 +2539,7 @@ MonoBehaviour: m_EditorClassIdentifier: _androidButtons: {fileID: 1167211826} _iosButtons: {fileID: 121492394} + _webglButtons: {fileID: 565393628} --- !u!1 &1703366541 GameObject: m_ObjectHideFlags: 0 @@ -2634,6 +2956,84 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2018121283} m_CullTransparentMesh: 0 +--- !u!1 &2070520012 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2070520013} + - component: {fileID: 2070520015} + - component: {fileID: 2070520014} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2070520013 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2070520012} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1313662201} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2070520014 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2070520012} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 147 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'Throw: JavaScript' +--- !u!222 &2070520015 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2070520012} + m_CullTransparentMesh: 0 --- !u!222 &2041051214740625425 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2862,7 +3262,7 @@ RectTransform: - {fileID: 2041051215832613689} - {fileID: 2041051216597842965} m_Father: {fileID: 1665572489} - m_RootOrder: 4 + m_RootOrder: 5 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 0} diff --git a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib new file mode 100644 index 000000000..e140a5d18 --- /dev/null +++ b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib @@ -0,0 +1,14 @@ +mergeInto(LibraryManager.library, { + + throwJavaScript: function () { + var something = undefined; + // Note: if we trigger the JS error by calling `something.do();` directly here, Unity get's stuck: + // An abnormal situation has occurred: the PlayerLoop internal function has been called recursively. Please contact Customer Support with a sample project so that we can reproduce the problem and troubleshoot it. + console.log("Scheduling a JavaScript error"); + setTimeout(function(){ + console.log("JavaScript error incoming..."); + something.do(); + }, 0); + }, + +}); diff --git a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib.meta b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib.meta new file mode 100644 index 000000000..a0f65ff9f --- /dev/null +++ b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/JavaScriptPlugin.jslib.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: dc7f3d07d56c4baaa9abaa89f51d5738 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/NativeSupportScene.cs b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/NativeSupportScene.cs index 958457036..2c17ed1ed 100644 --- a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/NativeSupportScene.cs +++ b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/NativeSupportScene.cs @@ -4,6 +4,7 @@ public class NativeSupportScene : MonoBehaviour { [SerializeField] private GameObject _androidButtons; [SerializeField] private GameObject _iosButtons; + [SerializeField] private GameObject _webglButtons; private void Start() { @@ -12,6 +13,10 @@ private void Start() #endif #if UNITY_EDITOR || !PLATFORM_IOS _iosButtons.SetActive(false); +#endif + // TODO: webgl native buttons support is currently not available - it requires a javascript error handling +#if true || UNITY_EDITOR || !PLATFORM_WEBGL + _webglButtons.SetActive(false); #endif } } diff --git a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs new file mode 100644 index 000000000..0f12226f1 --- /dev/null +++ b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs @@ -0,0 +1,20 @@ +using UnityEngine; +using System.Runtime.InteropServices; + +public class WebGLButtons : MonoBehaviour +{ + public void ThrowJavaScript() + { +#if PLATFORM_WEBGL + throwJavaScript(); +#else + Debug.Log("Requires WebGL."); +#endif + } + +#if PLATFORM_WEBGL + // JavaScriptPlugin.jslib + [DllImport("__Internal")] + private static extern void throwJavaScript(); +#endif +} diff --git a/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs.meta b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs.meta new file mode 100644 index 000000000..c63db2d94 --- /dev/null +++ b/samples/unity-of-bugs/Assets/Scripts/NativeSupport/WebGLButtons.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 088c449aca9f79c4b929eea17a498d7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs b/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs index 4cd458560..937d0208c 100644 --- a/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs +++ b/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using System.Web; using Sentry; using Sentry.Infrastructure; using Sentry.Unity; @@ -48,13 +49,17 @@ public void Start() private static string GetTestArg() { string arg = null; -#if UNITY_ANDROID +#if UNITY_EDITOR +#elif UNITY_ANDROID using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (var currentActivity = unityPlayer.GetStatic("currentActivity")) using (var intent = currentActivity.Call("getIntent")) { arg = intent.Call("getStringExtra", "test"); } +#elif UNITY_WEBGL + var uri = new Uri(Application.absoluteURL); + arg = HttpUtility.ParseQueryString(uri.Query).Get("test"); #else var args = Environment.GetCommandLineArgs(); if (args.Length > 2 && args[1] == "--test") @@ -104,7 +109,7 @@ public static void SmokeTest() t.ExpectMessage(currentMessage, "'type':'session'"); var guid = Guid.NewGuid().ToString(); - Debug.LogError(guid); + Debug.LogError($"LogError(GUID)={guid}"); // Skip the session init requests (there may be multiple of othem). We can't skip them by a "positive" // because they're also repeated with standard events (in an envelope). @@ -119,11 +124,11 @@ public static void SmokeTest() Debug.Log($"Done skipping non-event requests. Last one was: #{currentMessage}"); t.ExpectMessage(currentMessage, "'type':'event'"); - t.ExpectMessage(currentMessage, guid); + t.ExpectMessage(currentMessage, $"LogError(GUID)={guid}"); - SentrySdk.CaptureMessage(guid); + SentrySdk.CaptureMessage($"CaptureMessage(GUID)={guid}"); t.ExpectMessage(++currentMessage, "'type':'event'"); - t.ExpectMessage(currentMessage, guid); + t.ExpectMessage(currentMessage, $"CaptureMessage(GUID)={guid}"); var ex = new Exception("Exception & context test"); AddContext(); @@ -259,7 +264,10 @@ public void Pass() // Exit Code 200 to avoid false positive from a graceful exit unrelated to this test run exitCode = 200; + +#if !UNITY_WEBGL // We don't quit on WebGL because outgoing HTTP requests (in coroutines) would be cancelled. Application.Quit(exitCode); +#endif } } @@ -297,8 +305,14 @@ public string GetMessage(int index) public bool CheckMessage(int index, String substring) { +#if UNITY_WEBGL + // Note: we cannot use the standard checks on WebGL - it would get stuck here because of the lack of multi-threading. + // The verification is done in the python script used for WebGL smoke test + return true; +#else var message = GetMessage(index); return message.Contains(substring) || message.Contains(substring.Replace("'", "\"")); +#endif } public void ExpectMessage(int index, String substring) => diff --git a/samples/unity-of-bugs/Packages/manifest.json b/samples/unity-of-bugs/Packages/manifest.json index 0d63ad298..44fa10c4c 100644 --- a/samples/unity-of-bugs/Packages/manifest.json +++ b/samples/unity-of-bugs/Packages/manifest.json @@ -10,6 +10,7 @@ "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.audio": "1.0.0", "com.unity.modules.screencapture": "1.0.0", - "com.unity.modules.ui": "1.0.0" + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" } } diff --git a/samples/unity-of-bugs/Packages/packages-lock.json b/samples/unity-of-bugs/Packages/packages-lock.json index 568a6ef6d..6c5a63db9 100644 --- a/samples/unity-of-bugs/Packages/packages-lock.json +++ b/samples/unity-of-bugs/Packages/packages-lock.json @@ -128,6 +128,12 @@ "depth": 0, "source": "builtin", "dependencies": {} + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} } } } diff --git a/samples/unity-of-bugs/ProjectSettings/ProjectSettings.asset b/samples/unity-of-bugs/ProjectSettings/ProjectSettings.asset index de18da7a0..c178218cb 100644 --- a/samples/unity-of-bugs/ProjectSettings/ProjectSettings.asset +++ b/samples/unity-of-bugs/ProjectSettings/ProjectSettings.asset @@ -766,16 +766,16 @@ PlayerSettings: blurSplashScreenBackground: 1 spritePackerPolicy: webGLMemorySize: 16 - webGLExceptionSupport: 1 + webGLExceptionSupport: 3 webGLNameFilesAsHashes: 0 webGLDataCaching: 1 - webGLDebugSymbols: 0 + webGLDebugSymbols: 1 webGLEmscriptenArgs: webGLModulesDirectory: webGLTemplate: APPLICATION:Default webGLAnalyzeBuildSize: 0 webGLUseEmbeddedResources: 0 - webGLCompressionFormat: 1 + webGLCompressionFormat: 2 webGLLinkerTarget: 1 webGLThreadsSupport: 0 webGLWasmStreaming: 0 @@ -784,6 +784,7 @@ PlayerSettings: scriptingBackend: Android: 1 Standalone: 1 + WebGL: 1 il2cppCompilerConfiguration: Android: 1 managedStrippingLevel: {} diff --git a/scripts/smoke-test-webgl.py b/scripts/smoke-test-webgl.py new file mode 100644 index 000000000..7177ade3d --- /dev/null +++ b/scripts/smoke-test-webgl.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 + +# Testing approach: +# 1. Start a web=server for pre-built WebGL app directory (index.html & co) and to collect the API requests +# 3. Run the smoke test using chromedriver +# 4. Check the messages received by the API server + +import binascii +import datetime +import logging +import re +import time +import os +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + +host = '127.0.0.1' +port = 8000 +scriptDir = os.path.dirname(os.path.abspath(__file__)) +appDir = os.path.join(scriptDir, '..', 'samples', + 'artifacts', 'builds', 'WebGL') + +ignoreRegex = '"exception":{"values":\[{"type":"(' + '|'.join( + ['The resource [^ ]+ could not be loaded from the resource file!', 'GL.End requires material.SetPass before!']) + ')"' + + +class RequestVerifier: + __requests = [] + __testNumber = 0 + + def Capture(self, info, body): + match = re.search(ignoreRegex, body) + if match: + print( + "TEST: Skipping the received HTTP Request because it's an unrelated unity bug:\n{}".format(match.group(0))) + return + + print("TEST: Received HTTP Request #{} = {}\n{}".format( + len(self.__requests), info, body), flush=True) + self.__requests.append({"request": info, "body": body}) + + def Expect(self, message, result): + self.__testNumber += 1 + info = "TEST | #{}. {}: {}".format(self.__testNumber, + message, "PASS" if result else "FAIL") + if result: + print(info, flush=True) + else: + raise Exception(info) + + def ExpectMessage(self, index, substring): + message = self.__requests[index]["body"] + self.Expect("HTTP Request #{} contains \"{}\".".format(index, substring), + substring in message or substring.replace("'", "\"") in message) + + +t = RequestVerifier() + + +class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=appDir, **kwargs) + + def do_POST(self): + body = "" + content = self.rfile.read(int(self.headers['Content-Length'])) + try: + body = content.decode("utf-8") + except: + logging.exception("Exception while parsing an API request") + body = binascii.hexlify(bytearray(content)) + t.Capture(self.requestline, body) + self.send_response(HTTPStatus.OK, '{'+'}') + self.end_headers() + + +appServer = ThreadingHTTPServer((host, port), Handler) +appServerThread = Thread(target=appServer.serve_forever) +appServerThread.start() + + +class TestDriver: + def __init__(self): + options = Options() + options.add_experimental_option('excludeSwitches', ['enable-logging']) + options.add_argument('--headless') + d = DesiredCapabilities.CHROME + d['goog:loggingPrefs'] = {'browser': 'ALL'} + self.driver = webdriver.Chrome( + options=options, desired_capabilities=d) + self.driver.get('http://{}:{}?test=smoke'.format(host, port)) + self.messages = [] + + def fetchMessages(self): + for entry in self.driver.get_log('browser'): + m = entry['message'] + entry['message'] = m[m.find('"'):].replace('\\n', '').strip('" ') + self.messages.append(entry) + + def hasMessage(self, message): + self.fetchMessages() + return any(message in entry['message'] for entry in self.messages) + + def dumpMessages(self): + self.fetchMessages() + for entry in self.messages: + print("CHROME: {} {}".format(datetime.datetime.fromtimestamp( + entry['timestamp']/1000).strftime('%H:%M:%S.%f'), entry['message']), flush=True) + + +def waitUntil(condition, interval=0.1, timeout=1): + start = time.time() + while not condition(): + if time.time() - start >= timeout: + raise Exception('Waiting timed out'.format(condition)) + time.sleep(interval) + + +driver = TestDriver() +try: + waitUntil(lambda: driver.hasMessage('SMOKE TEST: PASS'), timeout=10) +finally: + driver.dumpMessages() + driver.driver.quit() + appServer.shutdown() + + +# Verify received API requests - see SmokeTester.cs - this is a copy-paste with minimal syntax changes +currentMessage = 0 +t.ExpectMessage(currentMessage, "'type':'session'") +currentMessage += 1 +t.ExpectMessage(currentMessage, "'type':'event'") +t.ExpectMessage(currentMessage, "LogError(GUID)") +currentMessage += 1 +t.ExpectMessage(currentMessage, "'type':'event'") +t.ExpectMessage(currentMessage, "CaptureMessage(GUID)") +currentMessage += 1 +t.ExpectMessage(currentMessage, "'type':'event'") +t.ExpectMessage( + currentMessage, "'message':'crumb','type':'error','data':{'foo':'bar'},'category':'bread','level':'critical'}") +t.ExpectMessage(currentMessage, "'message':'scope-crumb'}") +t.ExpectMessage(currentMessage, "'extra':{'extra-key':42}") +t.ExpectMessage(currentMessage, "'tags':{'tag-key':'tag-value'") +t.ExpectMessage( + currentMessage, "'user':{'email':'email@example.com','id':'user-id','ip_address':'::1','username':'username','other':{'role':'admin'}}") +print('TEST: PASS', flush=True) diff --git a/src/Sentry.Unity/UnityWebRequestTransport.cs b/src/Sentry.Unity/UnityWebRequestTransport.cs new file mode 100644 index 000000000..534b1b409 --- /dev/null +++ b/src/Sentry.Unity/UnityWebRequestTransport.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Sentry.Extensibility; +using Sentry.Http; +using Sentry.Protocol.Envelopes; +using UnityEngine; +using UnityEngine.Networking; + +namespace Sentry.Unity +{ + internal class WebBackgroundWorker : IBackgroundWorker + { + private readonly SentryMonoBehaviour _behaviour; + private readonly UnityWebRequestTransport _transport; + + public WebBackgroundWorker(SentryUnityOptions options, SentryMonoBehaviour behaviour) + { + _behaviour = behaviour; + _transport = new UnityWebRequestTransport(options); + } + + public bool EnqueueEnvelope(Envelope envelope) + { + _ = _behaviour.StartCoroutine(_transport.SendEnvelopeAsync(envelope)); + return true; + } + + public Task FlushAsync(TimeSpan timeout) => Task.CompletedTask; + + public int QueuedItems { get; } + } + + internal class UnityWebRequestTransport : HttpTransportBase + { + private readonly SentryUnityOptions _options; + + public UnityWebRequestTransport(SentryUnityOptions options) + : base(options) + => _options = options; + + // adapted HttpTransport.SendEnvelopeAsync() + internal IEnumerator SendEnvelopeAsync(Envelope envelope) + { + using var processedEnvelope = ProcessEnvelope(envelope); + if (processedEnvelope.Items.Count > 0) + { + // Send envelope to ingress + var httpRequest = CreateRequest(processedEnvelope); + var www = CreateWebRequest(httpRequest); + yield return www.SendWebRequest(); + + var response = GetResponse(www); + if (response is not null) + { + HandleResponse(response, processedEnvelope); + } + } + } + + private UnityWebRequest CreateWebRequest(HttpRequestMessage message) + { + using var contentStream = ReadStreamFromHttpContent(message.Content); + var contentMemoryStream = contentStream as MemoryStream; + if (contentMemoryStream is null) + { + contentMemoryStream = new MemoryStream(); + contentStream.CopyTo(contentMemoryStream); + contentMemoryStream.Flush(); + } + + var www = new UnityWebRequest + { + url = message.RequestUri.ToString(), + method = message.Method.Method.ToUpperInvariant(), + uploadHandler = new UploadHandlerRaw(contentMemoryStream.ToArray()), + downloadHandler = new DownloadHandlerBuffer() + }; + + foreach (var header in message.Headers) + { + www.SetRequestHeader(header.Key, string.Join(",", header.Value)); + } + + return www; + } + + private HttpResponseMessage? GetResponse(UnityWebRequest www) + { + // Let's disable treating "warning:obsolete" as an error here because the alternative of putting a static + // function to user code (to be able to use #if UNITY_2019) is just ugly. +#pragma warning disable 618 + // if (www.result == UnityWebRequest.Result.ConnectionError) // Unity 2020.1+; `.result` not present on 2019 + if (www.isNetworkError) // Unity 2019; obsolete (error) on later versions +#pragma warning restore 618 + { + _options.DiagnosticLogger?.LogWarning("Failed to send request: {0}", www.error); + return null; + } + + var response = new HttpResponseMessage((HttpStatusCode)www.responseCode); + foreach (var header in www.GetResponseHeaders()) + { + // Unity would throw if we tried to set content-type or content-length + if (header.Key is not "content-length" && header.Key is not "content-type") + { + response.Headers.Add(header.Key, header.Value); + } + } + response.Content = new StringContent(www.downloadHandler.text); + return response; + } + } +} diff --git a/src/Sentry.Unity/WebGL/SentryWebGL.cs b/src/Sentry.Unity/WebGL/SentryWebGL.cs new file mode 100644 index 000000000..b7df85bc4 --- /dev/null +++ b/src/Sentry.Unity/WebGL/SentryWebGL.cs @@ -0,0 +1,34 @@ +using Sentry.Extensibility; + +namespace Sentry.Unity.WebGL +{ + /// + /// Configure Sentry for WebGL + /// + public static class SentryWebGL + { + /// + /// Configures the WebGL support. + /// + /// The Sentry Unity options to use. + public static void Configure(SentryUnityOptions options) + { + options.DiagnosticLogger?.LogDebug("Updating configuration for Unity WebGL."); + + // Note: we need to use a custom background worker which actually doesn't work in the background + // because Unity doesn't support async (multithreading) yet. This may change in the future so let's watch + // https://docs.unity3d.com/2019.4/Documentation/ScriptReference/PlayerSettings.WebGL-threadsSupport.html + options.BackgroundWorker = new WebBackgroundWorker(options, SentryMonoBehaviour.Instance); + + // No way to recognize crashes in WebGL yet. We may be able to do so after implementing the JS support. + // Additionally, we could recognize the situation when the unity gets stuck due to an error in JS/native: + // "An abnormal situation has occurred: the PlayerLoop internal function has been called recursively. + // Please contact Customer Support with a sample project so that we can reproduce the problem and troubleshoot it." + // Maybe we could write a file when this error occurs and recognize it on the next start. Like unity-native. + options.CrashedLastRun = () => false; + + // Disable async when accessing files (e.g. FileStream(useAsync: true)) because it throws on WebGL. + options.UseAsyncFileIO = false; + } + } +} diff --git a/test/Scripts.Tests/package-release.zip.snapshot b/test/Scripts.Tests/package-release.zip.snapshot index 04b9ecdec..4cfb24a57 100644 --- a/test/Scripts.Tests/package-release.zip.snapshot +++ b/test/Scripts.Tests/package-release.zip.snapshot @@ -342,6 +342,8 @@ Samples~/unity-of-bugs/Scripts/NativeSupport/CppPlugin.cpp Samples~/unity-of-bugs/Scripts/NativeSupport/CppPlugin.cpp.meta Samples~/unity-of-bugs/Scripts/NativeSupport/IosButtons.cs Samples~/unity-of-bugs/Scripts/NativeSupport/IosButtons.cs.meta +Samples~/unity-of-bugs/Scripts/NativeSupport/JavaScriptPlugin.jslib +Samples~/unity-of-bugs/Scripts/NativeSupport/JavaScriptPlugin.jslib.meta Samples~/unity-of-bugs/Scripts/NativeSupport/KotlinPlugin.kt Samples~/unity-of-bugs/Scripts/NativeSupport/KotlinPlugin.kt.meta Samples~/unity-of-bugs/Scripts/NativeSupport/NativeButtons.cs @@ -350,6 +352,8 @@ Samples~/unity-of-bugs/Scripts/NativeSupport/NativeSupportScene.cs Samples~/unity-of-bugs/Scripts/NativeSupport/NativeSupportScene.cs.meta Samples~/unity-of-bugs/Scripts/NativeSupport/ObjectiveCPlugin.m Samples~/unity-of-bugs/Scripts/NativeSupport/ObjectiveCPlugin.m.meta +Samples~/unity-of-bugs/Scripts/NativeSupport/WebGLButtons.cs +Samples~/unity-of-bugs/Scripts/NativeSupport/WebGLButtons.cs.meta CHANGELOG.md CHANGELOG.md.meta Editor.meta