Skip to content

Commit 967dcc7

Browse files
Add Firebase Analytics session ID binding (#118)
* Add Firebase Analytics session ID binding * Use smoothed Analytics session callback
1 parent 08beaef commit 967dcc7

4 files changed

Lines changed: 113 additions & 1 deletion

File tree

source/Firebase/Analytics/ApiDefinition.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ interface Analytics
4040
[Export ("setSessionTimeoutInterval:")]
4141
void SetSessionTimeoutInterval (double sessionTimeoutInterval);
4242

43+
// + (void)sessionIDWithCompletion:(void (^)(int64_t sessionID, NSError *_Nullable error))completion;
44+
[Static]
45+
[Export ("sessionIDWithCompletion:")]
46+
void SessionIdWithCompletion (Action<long, NSError> completion);
47+
4348
// + (nullable NSString *)appInstanceID;
4449
[Static]
4550
[NullAllowed]

tests/E2E/Firebase.Foundation/FirebaseFoundationE2E/FirebaseRuntimeDriftCases.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
using System.Reflection;
22

3+
#if ENABLE_RUNTIME_DRIFT_CASE_ANALYTICS_SESSIONIDWITHCOMPLETION
4+
using Firebase.Analytics;
5+
using Foundation;
6+
using ObjCRuntime;
7+
#endif
8+
39
#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT
410
using Firebase.Database;
511
using Foundation;
@@ -84,6 +90,100 @@ public static async Task<string> ExecuteConfiguredCaseAsync()
8490
?.Value;
8591
}
8692

93+
#if ENABLE_RUNTIME_DRIFT_CASE_ANALYTICS_SESSIONIDWITHCOMPLETION
94+
static async Task<string> VerifyAnalyticsSessionIdWithCompletionAsync()
95+
{
96+
const string selector = "sessionIDWithCompletion:";
97+
98+
var signature = typeof(Analytics).GetMethod(
99+
nameof(Analytics.SessionIdWithCompletion),
100+
BindingFlags.Static | BindingFlags.Public,
101+
binder: null,
102+
types: new[] { typeof(Action<long, NSError>) },
103+
modifiers: null);
104+
if (signature is null)
105+
{
106+
throw new InvalidOperationException(
107+
$"Expected managed API '{nameof(Analytics.SessionIdWithCompletion)}({typeof(Action<long, NSError>).FullName})' was not found.");
108+
}
109+
110+
Analytics.SetAnalyticsCollectionEnabled(true);
111+
Analytics.SetConsent(new Dictionary<ConsentType, ConsentStatus>
112+
{
113+
[ConsentType.AnalyticsStorage] = ConsentStatus.Granted,
114+
[ConsentType.AdStorage] = ConsentStatus.Denied,
115+
});
116+
117+
var callbackInvoked = false;
118+
long callbackSessionId = 0;
119+
NSError? callbackError = null;
120+
NSException? marshaledException = null;
121+
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
122+
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
123+
124+
void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
125+
{
126+
marshaledException ??= args.Exception;
127+
marshaledExceptionMode ??= args.ExceptionMode;
128+
}
129+
130+
Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
131+
try
132+
{
133+
try
134+
{
135+
Analytics.SessionIdWithCompletion((sessionId, error) =>
136+
{
137+
callbackInvoked = true;
138+
callbackSessionId = sessionId;
139+
callbackError = error;
140+
completionSource.TrySetResult(true);
141+
});
142+
}
143+
catch (ObjCException ex)
144+
{
145+
throw new InvalidOperationException(
146+
$"Selector '{selector}' should not throw after the missing binding is added, but observed {ex.GetType().FullName}. " +
147+
$"Completion delegate type: {typeof(Action<long, NSError>).FullName}. " +
148+
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
149+
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
150+
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
151+
ex);
152+
}
153+
154+
var completedTask = await Task.WhenAny(completionSource.Task, Task.Delay(AsyncTimeout));
155+
if (completedTask != completionSource.Task)
156+
{
157+
throw new TimeoutException(
158+
$"Selector '{selector}' did not invoke its completion callback within {AsyncTimeout.TotalSeconds} seconds.");
159+
}
160+
161+
if (!callbackInvoked)
162+
{
163+
throw new InvalidOperationException(
164+
$"Selector '{selector}' completed without throwing, but the completion callback was never marked as invoked.");
165+
}
166+
167+
if (marshaledException is not null)
168+
{
169+
throw new InvalidOperationException(
170+
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
171+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
172+
}
173+
174+
return
175+
$"Selector '{selector}' invoked its completion callback without ObjC exception. " +
176+
$"Completion delegate type: {typeof(Action<long, NSError>).FullName}. " +
177+
$"SessionId: {callbackSessionId}. " +
178+
$"NSError: {callbackError?.LocalizedDescription ?? "<null>"}.";
179+
}
180+
finally
181+
{
182+
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
183+
}
184+
}
185+
#endif
186+
87187
#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT
88188
static Task<string> VerifyDatabaseServerValueIncrementAsync()
89189
{

tests/E2E/Firebase.Foundation/runtime-drift-cases.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"cases": [
3+
{
4+
"id": "analytics-sessionidwithcompletion",
5+
"method": "VerifyAnalyticsSessionIdWithCompletionAsync",
6+
"bindingPackage": "AdamE.Firebase.iOS.Analytics",
7+
"packages": []
8+
},
39
{
410
"id": "database-servervalue-increment",
511
"method": "VerifyDatabaseServerValueIncrementAsync",

tools/e2e/run-firebase-foundation.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ method = case.get("method")
124124
binding_package = case.get("bindingPackage")
125125
packages = case.get("packages", [])
126126
127-
if not method or not binding_package or not packages:
127+
if not method or not binding_package or packages is None:
128128
raise SystemExit(f"Runtime drift case '{case_id}' is missing required manifest fields.")
129129
130130
symbol = "ENABLE_RUNTIME_DRIFT_CASE_" + re.sub(r"[^A-Za-z0-9]+", "_", case_id).strip("_").upper()
@@ -153,6 +153,7 @@ PY
153153
runtime_drift_details=("${(@f)$(<"$runtime_drift_info")}")
154154
runtime_drift_method="${runtime_drift_details[1]}"
155155
runtime_drift_binding_package="${runtime_drift_details[2]}"
156+
required_packages+=("$runtime_drift_binding_package")
156157
for (( i = 3; i <= ${#runtime_drift_details[@]}; i++ )); do
157158
required_packages+=("${runtime_drift_details[$i]}")
158159
done

0 commit comments

Comments
 (0)