Skip to content

Commit 5272cb5

Browse files
authored
feat: add alerts triggers (#7)
1 parent aaba218 commit 5272cb5

25 files changed

+2247
-8
lines changed

example/basic/lib/main.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,48 @@ void main(List<String> args) {
206206
},
207207
);
208208

209+
// ==========================================================================
210+
// Firebase Alerts trigger examples
211+
// ==========================================================================
212+
213+
// Crashlytics new fatal issue alert
214+
firebase.alerts.crashlytics.onNewFatalIssuePublished(
215+
(event) async {
216+
final issue = event.data?.payload.issue;
217+
print('New fatal issue in Crashlytics:');
218+
print(' Issue ID: ${issue?.id}');
219+
print(' Title: ${issue?.title}');
220+
print(' App Version: ${issue?.appVersion}');
221+
print(' App ID: ${event.appId}');
222+
},
223+
);
224+
225+
// Billing plan update alert
226+
firebase.alerts.billing.onPlanUpdatePublished(
227+
(event) async {
228+
final payload = event.data?.payload;
229+
print('Billing plan updated:');
230+
print(' New Plan: ${payload?.billingPlan}');
231+
print(' Updated By: ${payload?.principalEmail}');
232+
print(' Type: ${payload?.notificationType}');
233+
},
234+
);
235+
236+
// Performance threshold alert with app ID filter
237+
firebase.alerts.performance.onThresholdAlertPublished(
238+
options: const AlertOptions(appId: '1:123456789:ios:abcdef'),
239+
(event) async {
240+
final payload = event.data?.payload;
241+
print('Performance threshold exceeded:');
242+
print(' Event: ${payload?.eventName}');
243+
print(' Metric: ${payload?.metricType}');
244+
print(
245+
' Threshold: ${payload?.thresholdValue} ${payload?.thresholdUnit}',
246+
);
247+
print(' Actual: ${payload?.violationValue} ${payload?.violationUnit}');
248+
},
249+
);
250+
209251
print('Functions registered successfully!');
210252
});
211253
}

lib/builder.dart

Lines changed: 233 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ class _TypeCheckers {
2828
TypeChecker.fromRuntime(ff.FirestoreNamespace);
2929
static final databaseNamespace =
3030
TypeChecker.fromRuntime(ff.DatabaseNamespace);
31+
static final alertsNamespace = TypeChecker.fromRuntime(ff.AlertsNamespace);
32+
static final crashlyticsNamespace =
33+
TypeChecker.fromRuntime(ff.CrashlyticsNamespace);
34+
static final billingNamespace = TypeChecker.fromRuntime(ff.BillingNamespace);
35+
static final appDistributionNamespace =
36+
TypeChecker.fromRuntime(ff.AppDistributionNamespace);
37+
static final performanceNamespace =
38+
TypeChecker.fromRuntime(ff.PerformanceNamespace);
3139
}
3240

3341
/// The main builder that generates functions.yaml.
@@ -144,6 +152,33 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
144152
}
145153
}
146154

155+
// Check for Alerts function declarations (main namespace)
156+
if (target != null && _isAlertsNamespace(target)) {
157+
if (methodName == 'onAlertPublished') {
158+
_extractGenericAlertFunction(node);
159+
}
160+
}
161+
162+
// Check for Crashlytics alert declarations
163+
if (target != null && _isCrashlyticsNamespace(target)) {
164+
_extractCrashlyticsAlertFunction(node, methodName);
165+
}
166+
167+
// Check for Billing alert declarations
168+
if (target != null && _isBillingNamespace(target)) {
169+
_extractBillingAlertFunction(node, methodName);
170+
}
171+
172+
// Check for App Distribution alert declarations
173+
if (target != null && _isAppDistributionNamespace(target)) {
174+
_extractAppDistributionAlertFunction(node, methodName);
175+
}
176+
177+
// Check for Performance alert declarations
178+
if (target != null && _isPerformanceNamespace(target)) {
179+
_extractPerformanceAlertFunction(node, methodName);
180+
}
181+
147182
// Check for parameter definitions (top-level function calls with no target)
148183
if (target == null && _isParamDefinition(methodName)) {
149184
_extractParameterFromMethod(node, methodName);
@@ -255,6 +290,41 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
255290
return _TypeCheckers.databaseNamespace.isExactlyType(staticType);
256291
}
257292

293+
/// Checks if the target is firebase.alerts.
294+
bool _isAlertsNamespace(Expression target) {
295+
final staticType = target.staticType;
296+
if (staticType == null) return false;
297+
return _TypeCheckers.alertsNamespace.isExactlyType(staticType);
298+
}
299+
300+
/// Checks if the target is firebase.alerts.crashlytics.
301+
bool _isCrashlyticsNamespace(Expression target) {
302+
final staticType = target.staticType;
303+
if (staticType == null) return false;
304+
return _TypeCheckers.crashlyticsNamespace.isExactlyType(staticType);
305+
}
306+
307+
/// Checks if the target is firebase.alerts.billing.
308+
bool _isBillingNamespace(Expression target) {
309+
final staticType = target.staticType;
310+
if (staticType == null) return false;
311+
return _TypeCheckers.billingNamespace.isExactlyType(staticType);
312+
}
313+
314+
/// Checks if the target is firebase.alerts.appDistribution.
315+
bool _isAppDistributionNamespace(Expression target) {
316+
final staticType = target.staticType;
317+
if (staticType == null) return false;
318+
return _TypeCheckers.appDistributionNamespace.isExactlyType(staticType);
319+
}
320+
321+
/// Checks if the target is firebase.alerts.performance.
322+
bool _isPerformanceNamespace(Expression target) {
323+
final staticType = target.staticType;
324+
if (staticType == null) return false;
325+
return _TypeCheckers.performanceNamespace.isExactlyType(staticType);
326+
}
327+
258328
/// Checks if this is a parameter definition function.
259329
bool _isParamDefinition(String name) =>
260330
name == 'defineString' ||
@@ -393,6 +463,152 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
393463
);
394464
}
395465

466+
/// Extracts a generic alert function declaration (onAlertPublished).
467+
void _extractGenericAlertFunction(MethodInvocation node) {
468+
// Extract alertType from named argument
469+
final alertTypeArg = _findNamedArg(node, 'alertType');
470+
if (alertTypeArg == null) return;
471+
472+
final alertTypeValue = _extractAlertTypeValue(alertTypeArg);
473+
if (alertTypeValue == null) return;
474+
475+
// Extract appId from options if present
476+
final optionsArg = _findNamedArg(node, 'options');
477+
String? appId;
478+
479+
if (optionsArg is InstanceCreationExpression) {
480+
appId = _extractStringField(optionsArg, 'appId');
481+
}
482+
483+
// Generate function name
484+
final sanitizedAlertType =
485+
alertTypeValue.replaceAll('.', '_').replaceAll('-', '');
486+
final functionName = 'onAlertPublished_$sanitizedAlertType';
487+
488+
endpoints[functionName] = _EndpointSpec(
489+
name: functionName,
490+
type: 'alert',
491+
alertType: alertTypeValue,
492+
appId: appId,
493+
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
494+
variableToParamName: _variableToParamName,
495+
);
496+
}
497+
498+
/// Extracts Crashlytics alert function declarations.
499+
void _extractCrashlyticsAlertFunction(
500+
MethodInvocation node,
501+
String methodName,
502+
) {
503+
// Map method names to alert types
504+
final alertType = switch (methodName) {
505+
'onNewFatalIssuePublished' => 'crashlytics.newFatalIssue',
506+
'onNewNonfatalIssuePublished' => 'crashlytics.newNonfatalIssue',
507+
'onRegressionAlertPublished' => 'crashlytics.regression',
508+
'onStabilityDigestPublished' => 'crashlytics.stabilityDigest',
509+
'onVelocityAlertPublished' => 'crashlytics.velocity',
510+
'onNewAnrIssuePublished' => 'crashlytics.newAnrIssue',
511+
_ => null,
512+
};
513+
514+
if (alertType == null) return;
515+
516+
_extractAlertEndpoint(node, alertType);
517+
}
518+
519+
/// Extracts Billing alert function declarations.
520+
void _extractBillingAlertFunction(MethodInvocation node, String methodName) {
521+
final alertType = switch (methodName) {
522+
'onPlanUpdatePublished' => 'billing.planUpdate',
523+
'onPlanAutomatedUpdatePublished' => 'billing.planAutomatedUpdate',
524+
_ => null,
525+
};
526+
527+
if (alertType == null) return;
528+
529+
_extractAlertEndpoint(node, alertType);
530+
}
531+
532+
/// Extracts App Distribution alert function declarations.
533+
void _extractAppDistributionAlertFunction(
534+
MethodInvocation node,
535+
String methodName,
536+
) {
537+
final alertType = switch (methodName) {
538+
'onNewTesterIosDevicePublished' => 'appDistribution.newTesterIosDevice',
539+
'onInAppFeedbackPublished' => 'appDistribution.inAppFeedback',
540+
_ => null,
541+
};
542+
543+
if (alertType == null) return;
544+
545+
_extractAlertEndpoint(node, alertType);
546+
}
547+
548+
/// Extracts Performance alert function declarations.
549+
void _extractPerformanceAlertFunction(
550+
MethodInvocation node,
551+
String methodName,
552+
) {
553+
final alertType = switch (methodName) {
554+
'onThresholdAlertPublished' => 'performance.threshold',
555+
_ => null,
556+
};
557+
558+
if (alertType == null) return;
559+
560+
_extractAlertEndpoint(node, alertType);
561+
}
562+
563+
/// Helper to extract alert endpoint from a method invocation.
564+
void _extractAlertEndpoint(MethodInvocation node, String alertType) {
565+
// Extract appId from options if present
566+
final optionsArg = _findNamedArg(node, 'options');
567+
String? appId;
568+
569+
if (optionsArg is InstanceCreationExpression) {
570+
appId = _extractStringField(optionsArg, 'appId');
571+
}
572+
573+
// Generate function name
574+
final sanitizedAlertType =
575+
alertType.replaceAll('.', '_').replaceAll('-', '');
576+
final functionName = 'onAlertPublished_$sanitizedAlertType';
577+
578+
endpoints[functionName] = _EndpointSpec(
579+
name: functionName,
580+
type: 'alert',
581+
alertType: alertType,
582+
appId: appId,
583+
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
584+
variableToParamName: _variableToParamName,
585+
);
586+
}
587+
588+
/// Extracts alert type value from an expression.
589+
String? _extractAlertTypeValue(Expression expression) {
590+
if (expression is InstanceCreationExpression) {
591+
// Extract from constructor: const CrashlyticsNewFatalIssue()
592+
final typeName = expression.constructorName.type.name2.lexeme;
593+
return switch (typeName) {
594+
'CrashlyticsNewFatalIssue' => 'crashlytics.newFatalIssue',
595+
'CrashlyticsNewNonfatalIssue' => 'crashlytics.newNonfatalIssue',
596+
'CrashlyticsRegression' => 'crashlytics.regression',
597+
'CrashlyticsStabilityDigest' => 'crashlytics.stabilityDigest',
598+
'CrashlyticsVelocity' => 'crashlytics.velocity',
599+
'CrashlyticsNewAnrIssue' => 'crashlytics.newAnrIssue',
600+
'BillingPlanUpdate' => 'billing.planUpdate',
601+
'BillingPlanAutomatedUpdate' => 'billing.planAutomatedUpdate',
602+
'AppDistributionNewTesterIosDevice' =>
603+
'appDistribution.newTesterIosDevice',
604+
'AppDistributionInAppFeedback' => 'appDistribution.inAppFeedback',
605+
'PerformanceThreshold' => 'performance.threshold',
606+
_ => null,
607+
};
608+
}
609+
return null;
610+
}
611+
396612
/// Extracts a parameter definition from FunctionExpressionInvocation.
397613
void _extractParameter(
398614
FunctionExpressionInvocation node,
@@ -598,11 +814,14 @@ class _EndpointSpec {
598814
this.databaseEventType,
599815
this.refPath,
600816
this.instance,
817+
this.alertType,
818+
this.appId,
601819
this.options,
602820
this.variableToParamName = const {},
603821
});
604822
final String name;
605-
final String type; // 'https', 'callable', 'pubsub', 'firestore', 'database'
823+
// 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert'
824+
final String type;
606825
final String? topic; // For Pub/Sub functions
607826
final String? firestoreEventType; // For Firestore: onDocumentCreated, etc.
608827
final String? documentPath; // For Firestore: users/{userId}
@@ -611,6 +830,8 @@ class _EndpointSpec {
611830
final String? databaseEventType; // For Database: onValueCreated, etc.
612831
final String? refPath; // For Database: /users/{userId}
613832
final String? instance; // For Database: database instance or '*'
833+
final String? alertType; // For Alerts: crashlytics.newFatalIssue, etc.
834+
final String? appId; // For Alerts: optional app ID filter
614835
final InstanceCreationExpression? options;
615836
final Map<String, String> variableToParamName;
616837

@@ -1345,6 +1566,17 @@ String _generateYaml(
13451566
buffer.writeln(' ref: "$normalizedRef"');
13461567
buffer.writeln(' instance: "${endpoint.instance ?? '*'}"');
13471568

1569+
buffer.writeln(' retry: false');
1570+
} else if (endpoint.type == 'alert' && endpoint.alertType != null) {
1571+
buffer.writeln(' eventTrigger:');
1572+
buffer.writeln(
1573+
' eventType: "google.firebase.firebasealerts.alerts.v1.published"',
1574+
);
1575+
buffer.writeln(' eventFilters:');
1576+
buffer.writeln(' alerttype: "${endpoint.alertType}"');
1577+
if (endpoint.appId != null) {
1578+
buffer.writeln(' appid: "${endpoint.appId}"');
1579+
}
13481580
buffer.writeln(' retry: false');
13491581
}
13501582
}

lib/firebase_functions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export 'package:shelf/shelf.dart' show Request, Response;
6161

6262
// Re-export built-in params from params.dart for convenience
6363
export 'params.dart' show databaseURL, gcloudProject, projectID, storageBucket;
64+
// Alerts triggers
65+
export 'src/alerts/alerts.dart';
6466
// Common types
6567
export 'src/common/cloud_event.dart';
6668
export 'src/common/expression.dart';

0 commit comments

Comments
 (0)