Skip to content

Commit 72cf15b

Browse files
committed
feat: add warnings when using experimental triggers in Dart runtime
1 parent 92fcb91 commit 72cf15b

3 files changed

Lines changed: 263 additions & 1 deletion

File tree

src/deploy/functions/prepare.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
groupEndpointsByCodebase,
3131
targetCodebases,
3232
} from "./functionsDeployHelper";
33-
import { logLabeledBullet } from "../../utils";
33+
import { logLabeledBullet, logLabeledWarning } from "../../utils";
34+
import { isDartEndpoint, classifyEndpoints } from "./runtimes/dart/triggerSupport";
3435
import { getFunctionsConfig, prepareFunctionsUpload } from "./prepareFunctionsUpload";
3536
import { promptForFailurePolicies, promptForMinInstances } from "./prompts";
3637
import { needProjectId, needProjectNumber } from "../../projectUtils";
@@ -292,6 +293,7 @@ export async function prepare(
292293

293294
await ensureAllRequiredAPIsEnabled(projectNumber, wantBackend);
294295
await warnIfNewGenkitFunctionIsMissingSecrets(wantBackend, haveBackend, options);
296+
warnIfDartBackendHasUnsupportedTriggers(wantBackend);
295297

296298
// ===Phase 6. Ask for user prompts for things might warrant user attentions.
297299
// We limit the scope endpoints being deployed.
@@ -541,6 +543,29 @@ export async function loadCodebases(
541543
return wantBuilds;
542544
}
543545

546+
/**
547+
* Warns when a Dart backend contains triggers that are not yet
548+
* production-ready. Classification is owned by the shared
549+
* `dart/triggerSupport` module.
550+
*/
551+
function warnIfDartBackendHasUnsupportedTriggers(want: backend.Backend): void {
552+
const dartEndpoints = backend.allEndpoints(want).filter(isDartEndpoint);
553+
if (dartEndpoints.length === 0) {
554+
return;
555+
}
556+
557+
const { emulatorOnly, experimental } = classifyEndpoints(dartEndpoints);
558+
const unsupported = [...emulatorOnly, ...experimental];
559+
if (unsupported.length > 0) {
560+
logLabeledWarning(
561+
"functions",
562+
`The following Dart functions use triggers that are not yet supported for production deployment: ${unsupported.map((ep) => ep.id).join(", ")}. ` +
563+
"They will be deployed but may not work as expected. " +
564+
"See https://github.com/firebase/firebase-functions-dart#status-alpha-v010 for current trigger support.",
565+
);
566+
}
567+
}
568+
544569
// Genkit almost always requires an API key, so warn if the customer is about to deploy
545570
// a function and doesn't have one. To avoid repetitive nagging, only warn on the first
546571
// deploy of the function.
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Dart trigger support classification.
3+
*
4+
* Firebase Functions for Dart is in alpha. Only HTTPS triggers are
5+
* production-ready today. Other trigger types have varying levels of
6+
* support as documented in the firebase-functions-dart README.
7+
*
8+
* This module is the single source of truth for that classification and
9+
* is consumed by both the emulator and the deploy pipeline so warnings
10+
* stay consistent.
11+
*/
12+
13+
import * as backend from "../../backend";
14+
import * as supported from "../supported";
15+
import { Constants } from "../../../../emulator/constants";
16+
17+
// ---------------------------------------------------------------------------
18+
// Support levels
19+
// ---------------------------------------------------------------------------
20+
21+
/**
22+
* How well a Dart trigger type is supported today.
23+
*
24+
* - `production` – works in both the emulator and production deployments.
25+
* - `emulatorOnly` – works in the Firebase emulator but cannot be deployed
26+
* to production yet.
27+
* - `experimental` – implemented in the Dart SDK but not yet supported by
28+
* the emulator or production. APIs may change.
29+
*/
30+
export type DartTriggerSupportLevel = "production" | "emulatorOnly" | "experimental";
31+
32+
// ---------------------------------------------------------------------------
33+
// Service → support-level maps
34+
// ---------------------------------------------------------------------------
35+
36+
/** Event-trigger services that work in the emulator only. */
37+
const EMULATOR_ONLY_SERVICES: ReadonlySet<string> = new Set([
38+
Constants.SERVICE_FIRESTORE,
39+
Constants.SERVICE_REALTIME_DATABASE,
40+
Constants.SERVICE_STORAGE,
41+
]);
42+
43+
/** Event-trigger services that are experimental. */
44+
const EXPERIMENTAL_SERVICES: ReadonlySet<string> = new Set([
45+
Constants.SERVICE_PUBSUB,
46+
Constants.SERVICE_EVENTARC,
47+
Constants.SERVICE_AUTH,
48+
Constants.SERVICE_FIREALERTS,
49+
Constants.SERVICE_REMOTE_CONFIG,
50+
Constants.SERVICE_TEST_LAB,
51+
Constants.SERVICE_CLOUD_TASKS,
52+
]);
53+
54+
// ---------------------------------------------------------------------------
55+
// Public helpers
56+
// ---------------------------------------------------------------------------
57+
58+
/**
59+
* Returns the support level for a Dart endpoint's trigger type.
60+
*
61+
* The classification intentionally matches the status table in the
62+
* firebase-functions-dart README:
63+
* | Status | Triggers |
64+
* |-------------------|----------------------------------------------------|
65+
* | ✅ Production | HTTPS (`onRequest`, `onCall`, `onCallWithData`) |
66+
* | ⚠️ Emulator only | Firestore, Realtime Database, Storage |
67+
* | 🚧 Experimental | Pub/Sub, Scheduler, Alerts, Eventarc, Identity, |
68+
* | | Remote Config, Test Lab, Tasks |
69+
*/
70+
export function endpointSupportLevel(ep: backend.Endpoint): DartTriggerSupportLevel {
71+
// HTTPS and callable triggers are production-ready.
72+
if (backend.isHttpsTriggered(ep) || backend.isCallableTriggered(ep)) {
73+
// Task-queue functions look like HTTPS but are experimental.
74+
if (backend.isTaskQueueTriggered(ep)) {
75+
return "experimental";
76+
}
77+
return "production";
78+
}
79+
80+
// Scheduled triggers are experimental (emulator converts them to pubsub).
81+
if (backend.isScheduleTriggered(ep)) {
82+
return "experimental";
83+
}
84+
85+
// Blocking triggers (Identity Platform) are experimental.
86+
if (backend.isBlockingTriggered(ep)) {
87+
return "experimental";
88+
}
89+
90+
// Remaining event triggers — classify by service.
91+
if (backend.isEventTriggered(ep)) {
92+
const service = ep.eventTrigger.eventType
93+
? serviceFromEventType(ep.eventTrigger.eventType)
94+
: undefined;
95+
if (service && EMULATOR_ONLY_SERVICES.has(service)) {
96+
return "emulatorOnly";
97+
}
98+
if (service && EXPERIMENTAL_SERVICES.has(service)) {
99+
return "experimental";
100+
}
101+
}
102+
103+
// Unknown trigger.
104+
return "experimental";
105+
}
106+
107+
/**
108+
* Returns `true` when the given endpoint belongs to a Dart runtime.
109+
*/
110+
export function isDartEndpoint(ep: backend.Endpoint): boolean {
111+
return supported.runtimeIsLanguage(ep.runtime, "dart");
112+
}
113+
114+
/**
115+
* Partitions a list of Dart endpoints by their support level.
116+
*
117+
* Only non-production endpoints are returned — callers never need to
118+
* enumerate production-ready functions.
119+
*/
120+
export function classifyEndpoints(endpoints: backend.Endpoint[]): {
121+
emulatorOnly: backend.Endpoint[];
122+
experimental: backend.Endpoint[];
123+
} {
124+
const emulatorOnly: backend.Endpoint[] = [];
125+
const experimental: backend.Endpoint[] = [];
126+
127+
for (const ep of endpoints) {
128+
switch (endpointSupportLevel(ep)) {
129+
case "production":
130+
break;
131+
case "emulatorOnly":
132+
emulatorOnly.push(ep);
133+
break;
134+
case "experimental":
135+
experimental.push(ep);
136+
break;
137+
}
138+
}
139+
140+
return { emulatorOnly, experimental };
141+
}
142+
143+
/**
144+
* Returns a human-readable trigger-type label for a Dart endpoint.
145+
*
146+
* Used to produce grouped warning messages like
147+
* `Dart **firestore** triggers work in the emulator but …`
148+
*/
149+
export function triggerTypeLabel(ep: backend.Endpoint): string {
150+
if (backend.isScheduleTriggered(ep)) return "scheduler";
151+
if (backend.isTaskQueueTriggered(ep)) return "tasks";
152+
if (backend.isBlockingTriggered(ep)) return "identity";
153+
if (backend.isCallableTriggered(ep)) return "callable";
154+
if (backend.isHttpsTriggered(ep)) return "https";
155+
if (backend.isEventTriggered(ep)) {
156+
const svc = serviceFromEventType(ep.eventTrigger.eventType);
157+
return svc ? Constants.getServiceName(svc) : ep.eventTrigger.eventType;
158+
}
159+
return "unknown";
160+
}
161+
162+
/**
163+
* Groups endpoints by their {@link triggerTypeLabel} and returns a
164+
* `Map<label, endpointIds[]>` suitable for building warning messages.
165+
*/
166+
export function groupByTriggerLabel(endpoints: backend.Endpoint[]): Map<string, string[]> {
167+
const groups = new Map<string, string[]>();
168+
for (const ep of endpoints) {
169+
const label = triggerTypeLabel(ep);
170+
const ids = groups.get(label) ?? [];
171+
ids.push(ep.id);
172+
groups.set(label, ids);
173+
}
174+
return groups;
175+
}
176+
177+
// ---------------------------------------------------------------------------
178+
// Internal helpers
179+
// ---------------------------------------------------------------------------
180+
181+
/**
182+
* Rough mapping from a CloudEvent `eventType` string to a service constant.
183+
*
184+
* This mirrors the logic in `functionsEmulatorShared.getServiceFromEventType`
185+
* but is kept local so the module stays self-contained.
186+
*/
187+
function serviceFromEventType(eventType: string): string | undefined {
188+
if (eventType.includes("firestore")) return Constants.SERVICE_FIRESTORE;
189+
if (eventType.includes("database")) return Constants.SERVICE_REALTIME_DATABASE;
190+
if (eventType.includes("pubsub")) return Constants.SERVICE_PUBSUB;
191+
if (eventType.includes("storage")) return Constants.SERVICE_STORAGE;
192+
if (eventType.includes("firebasealerts")) return Constants.SERVICE_FIREALERTS;
193+
if (eventType.includes("auth")) return Constants.SERVICE_AUTH;
194+
if (eventType.includes("remoteconfig")) return Constants.SERVICE_REMOTE_CONFIG;
195+
if (eventType.includes("testlab") || eventType.includes("testing"))
196+
return Constants.SERVICE_TEST_LAB;
197+
return undefined;
198+
}

src/emulator/functionsEmulator.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ import { getCredentialsEnvironment, setEnvVarsForEmulators } from "./env";
6262
import { runWithVirtualEnv } from "../functions/python";
6363
import { runtimeIsLanguage, Runtime } from "../deploy/functions/runtimes/supported";
6464
import { DART_ENTRY_POINT } from "../deploy/functions/runtimes/dart";
65+
import {
66+
isDartEndpoint,
67+
classifyEndpoints,
68+
groupByTriggerLabel,
69+
} from "../deploy/functions/runtimes/dart/triggerSupport";
6570
import { ExtensionsEmulator } from "./extensionsEmulator";
6671

6772
const EVENT_INVOKE_GA4 = "functions_invoke"; // event name GA4 (alphanumertic)
@@ -634,6 +639,10 @@ export class FunctionsEmulator implements EmulatorInstance {
634639
for (const e of endpoints) {
635640
e.codebase = emulatableBackend.codebase;
636641
}
642+
643+
// Warn about Dart triggers with limited support.
644+
this.logDartTriggerSupportWarnings(endpoints);
645+
637646
return emulatedFunctionsFromEndpoints(endpoints);
638647
}
639648
}
@@ -867,6 +876,36 @@ export class FunctionsEmulator implements EmulatorInstance {
867876
}
868877
}
869878

879+
/**
880+
* Logs warnings for Dart endpoints whose trigger types are not yet
881+
* production-ready. Classification is owned by the shared
882+
* `dart/triggerSupport` module.
883+
*/
884+
private logDartTriggerSupportWarnings(endpoints: backend.Endpoint[]): void {
885+
const dartEndpoints = endpoints.filter(isDartEndpoint);
886+
if (dartEndpoints.length === 0) {
887+
return;
888+
}
889+
890+
const { emulatorOnly, experimental } = classifyEndpoints(dartEndpoints);
891+
892+
groupByTriggerLabel(emulatorOnly).forEach((ids, label) => {
893+
this.logger.logLabeled(
894+
"WARN",
895+
"functions",
896+
`Dart ${clc.bold(label)} triggers work in the emulator but cannot be deployed to production yet: ${ids.join(", ")}`,
897+
);
898+
});
899+
900+
groupByTriggerLabel(experimental).forEach((ids, label) => {
901+
this.logger.logLabeled(
902+
"WARN",
903+
"functions",
904+
`Dart ${clc.bold(label)} triggers are experimental and not yet fully supported: ${ids.join(", ")}`,
905+
);
906+
});
907+
}
908+
870909
// Currently only cleans up eventarc and firealerts triggers
871910
async removeTriggers(toRemove: string[]) {
872911
for (const triggerKey of toRemove) {

0 commit comments

Comments
 (0)