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