From b8e18c77bc86ffd10e0af83c858c8b3947b183fa Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 19 Dec 2023 16:34:39 +0800 Subject: [PATCH 01/17] Adding configuration for gemeni API key --- extension.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extension.yaml b/extension.yaml index a26f73d..1ec5d58 100644 --- a/extension.yaml +++ b/extension.yaml @@ -106,6 +106,12 @@ params: type: secret required: false + - param: API_KEY_GEMENI + label: Google Gemini API key + description: You can create an API key in Google AI Studio. + type: secret + required: false + resources: - name: anr type: firebaseextensions.v1beta.v2function From 92b8c1aed60cdc935a917354f16d807c99710ae2 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Wed, 20 Dec 2023 14:07:33 +0800 Subject: [PATCH 02/17] Adding initial Gemeni Service --- functions/package.json | 1 + functions/src/services/gemeni.service.ts | 76 ++++++++++++++++++++++++ functions/yarn.lock | 5 ++ 3 files changed, 82 insertions(+) create mode 100644 functions/src/services/gemeni.service.ts diff --git a/functions/package.json b/functions/package.json index c6a25aa..fed8a22 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,6 +18,7 @@ }, "main": "lib/src/index.js", "dependencies": { + "@google/generative-ai": "^0.1.3", "firebase-admin": "^11.4.1", "firebase-functions": "^4.1.1", "request": "^2.88.2" diff --git a/functions/src/services/gemeni.service.ts b/functions/src/services/gemeni.service.ts new file mode 100644 index 0000000..01c6b17 --- /dev/null +++ b/functions/src/services/gemeni.service.ts @@ -0,0 +1,76 @@ +import { + GoogleGenerativeAI, + HarmCategory, + HarmBlockThreshold, +} from "@google/generative-ai"; +import { SupportedCrashlyticsEvent } from "../models/app-crash"; + +export class GemeniService { + constructor(apiKey: string) { + this.genAI = new GoogleGenerativeAI(apiKey); + } + + private genAI: GoogleGenerativeAI; + private static readonly MODEL_NAME = "gemini-pro"; + private static readonly GENERATION_CONFIG = { + temperature: 0.9, + topK: 1, + topP: 1, + maxOutputTokens: 2048, + }; + private static readonly SAFETY_SETTINGS = [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, + }, + ]; + + async explainCrash(appCrash: SupportedCrashlyticsEvent): Promise { + const model = this.genAI.getGenerativeModel({ model: GemeniService.MODEL_NAME }); + + const promptContext = ` + @type defines the type of crashlytics issue. + + CrashlyticsRegressionAlertPayload is an issue that was previously fixed but has reappeared. + resolveTime - The time that the Crashlytics issues was most recently resolved before it began to reoccur. + + CrashlyticsNewNonfatalIssuePayload is a Non Fatal issue. + + CrashlyticsNewFatalIssuePayload is a Fatal issue. + + CrashlyticsNewAnrIssuePayload is an Application Not Responding issue. + `; + + const promptExplanation = ` + Explain the the following crashlytics issue for a software developer. + Explain the issue in a way that is easy to understand and actionable. + Use the following JSON information to explain the issue: + `; + + const crash = JSON.stringify(appCrash) + + const parts = [ + {text: [promptContext, promptExplanation, crash].join("\n\n")}, + ]; + + const result = await model.generateContent({ + contents: [{ role: "user", parts }], + generationConfig: GemeniService.GENERATION_CONFIG, + safetySettings: GemeniService.SAFETY_SETTINGS, + }); + + return result.response.text(); + } +} \ No newline at end of file diff --git a/functions/yarn.lock b/functions/yarn.lock index 3d3b7a3..8771b7c 100644 --- a/functions/yarn.lock +++ b/functions/yarn.lock @@ -702,6 +702,11 @@ teeny-request "^8.0.0" uuid "^8.0.0" +"@google/generative-ai@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.1.3.tgz#8e529d4d86c85b64d297b4abf1a653d613a09a9f" + integrity sha512-Cm4uJX1sKarpm1mje/MiOIinM7zdUUrQp/5/qGPAgznbdd/B9zup5ehT6c1qGqycFcSopTA1J1HpqHS5kJR8hQ== + "@grpc/grpc-js@^1.0.0", "@grpc/grpc-js@^1.3.2", "@grpc/grpc-js@~1.7.0": version "1.7.3" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.7.3.tgz#f2ea79f65e31622d7f86d4b4c9ae38f13ccab99a" From bc56ef19e7b47fb41656312a466817e6ce0205c5 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:10:19 +0800 Subject: [PATCH 03/17] Adding explanation free text to an app crash --- functions/src/models/app-crash.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/src/models/app-crash.ts b/functions/src/models/app-crash.ts index 64b8757..becde12 100644 --- a/functions/src/models/app-crash.ts +++ b/functions/src/models/app-crash.ts @@ -52,6 +52,7 @@ export class AppCrash implements IAppCrash { * @return {AppCrash} An instance of `AppCrash` class */ public static fromCrashlytics(event: SupportedCrashlyticsEvent): AppCrash { + const appCrash = { issueId: event.data.payload.issue.id, issueTitle: event.data.payload.issue.title, @@ -83,5 +84,6 @@ export class AppCrash implements IAppCrash { public readonly issueTitle: string; public readonly appId: string; public readonly appVersion: string; - public readonly tags = ["bug"]; + public readonly tags = ["bug"]; + public explanation?: string; } From d9321cdd4395289e12ac1cc7c65a9d3a98820da0 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:10:32 +0800 Subject: [PATCH 04/17] Adding Gemeni API key to config --- functions/src/utils/env-config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/functions/src/utils/env-config.ts b/functions/src/utils/env-config.ts index cd6582e..7eb91ec 100644 --- a/functions/src/utils/env-config.ts +++ b/functions/src/utils/env-config.ts @@ -56,6 +56,13 @@ export class EnvConfig { ].filter((x) => !!x) as string[]; } + /** + * Get Google Gemeni API key + */ + static get apiKeyGemeni(): string | undefined { + return process.env.API_KEY_GEMENI; + } + /** * Get an environment variable's value From 62cb1efae1f7923ddc95c1175e8ad16e0e8712ea Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:10:44 +0800 Subject: [PATCH 05/17] Updating Gemeni service --- functions/src/services/gemeni.service.ts | 37 ++++++++++-------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/functions/src/services/gemeni.service.ts b/functions/src/services/gemeni.service.ts index 01c6b17..43c8c1d 100644 --- a/functions/src/services/gemeni.service.ts +++ b/functions/src/services/gemeni.service.ts @@ -1,10 +1,11 @@ import { GoogleGenerativeAI, - HarmCategory, - HarmBlockThreshold, } from "@google/generative-ai"; import { SupportedCrashlyticsEvent } from "../models/app-crash"; +/** + * Implements a service for interacting with Gemeni LLM + */ export class GemeniService { constructor(apiKey: string) { this.genAI = new GoogleGenerativeAI(apiKey); @@ -18,25 +19,17 @@ export class GemeniService { topP: 1, maxOutputTokens: 2048, }; - private static readonly SAFETY_SETTINGS = [ - { - category: HarmCategory.HARM_CATEGORY_HARASSMENT, - threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, - }, - { - category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, - }, - { - category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, - threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, - }, - { - category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, - threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, - }, - ]; + // Block harmful content, hate speech etc + // Should not be needed or applicable to this use case + private static readonly SAFETY_SETTINGS = []; + + /** + * Make Gemeni LLM explain a crashlytics event. + * + * @param {SupportedCrashlyticsEvent} appCrash App crash information + * @returns {Promise} Promise with the explanation + */ async explainCrash(appCrash: SupportedCrashlyticsEvent): Promise { const model = this.genAI.getGenerativeModel({ model: GemeniService.MODEL_NAME }); @@ -59,10 +52,10 @@ export class GemeniService { Use the following JSON information to explain the issue: `; - const crash = JSON.stringify(appCrash) + const crashJson = JSON.stringify(appCrash) const parts = [ - {text: [promptContext, promptExplanation, crash].join("\n\n")}, + {text: [promptContext, promptExplanation, crashJson].join("\n\n")}, ]; const result = await model.generateContent({ From 096da32562d3861761e93b2e52ccf601e05e4433 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:11:01 +0800 Subject: [PATCH 06/17] Implementing usage of the gemeni service --- functions/src/alerts/crashlytics.ts | 36 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index 940dd2c..d43b379 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -11,10 +11,11 @@ import {EnvConfig} from "../utils/env-config"; import {DiscordWebhook} from "../webhook-plugins/discord"; import {GoogleChatWebhook} from "../webhook-plugins/google-chat"; import {SlackWebhook} from "../webhook-plugins/slack"; +import { GemeniService } from "../services/gemeni.service"; const functionOpts = { region: process.env.LOCATION, - secrets: ["WEBHOOK_MANDATORY", "WEBHOOK_OPTIONAL"], + secrets: ["WEBHOOK_MANDATORY", "WEBHOOK_OPTIONAL", "API_KEY_GEMENI"], }; /** @@ -114,8 +115,14 @@ async function handleCrashlyticsEvent(appCrash: AppCrash): export const anr = crashlytics.onNewAnrIssuePublished(functionOpts, async (event) => { logger.debug("onNewAnrIssuePublished", event); - + const appCrash = AppCrash.fromCrashlytics(event); + if (EnvConfig.apiKeyGemeni) { + logger.debug("Call Gemeni API for explanation"); + const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); + appCrash.explanation = await gemeniService.explainCrash(event); + logger.debug("Gemeni explanation", appCrash.explanation); + } appCrash.tags.push("critical"); @@ -123,11 +130,16 @@ export const anr = }); export const fatal = - crashlytics.onNewFatalIssuePublished(functionOpts, (event) => { + crashlytics.onNewFatalIssuePublished(functionOpts, async (event) => { logger.debug("onNewFatalIssuePublished", event); const appCrash = AppCrash.fromCrashlytics(event); - + if (EnvConfig.apiKeyGemeni) { + logger.debug("Call Gemeni API for explanation"); + const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); + appCrash.explanation = await gemeniService.explainCrash(event); + logger.debug("Gemeni explanation", appCrash.explanation); + } appCrash.tags.push("critical"); return handleCrashlyticsEvent(appCrash); @@ -135,19 +147,31 @@ export const fatal = export const nonfatal = - crashlytics.onNewNonfatalIssuePublished(functionOpts, (event) => { + crashlytics.onNewNonfatalIssuePublished(functionOpts, async (event) => { logger.debug("onNewNonfatalIssuePublished", event); const appCrash = AppCrash.fromCrashlytics(event); + if (EnvConfig.apiKeyGemeni) { + logger.debug("Call Gemeni API for explanation"); + const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); + appCrash.explanation = await gemeniService.explainCrash(event); + logger.debug("Gemeni explanation", appCrash.explanation); + } return handleCrashlyticsEvent(appCrash); }); export const regression = - crashlytics.onRegressionAlertPublished(functionOpts, (event) => { + crashlytics.onRegressionAlertPublished(functionOpts, async (event) => { logger.debug("onRegressionAlertPublished", event); const appCrash = AppCrash.fromCrashlytics(event); + if (EnvConfig.apiKeyGemeni) { + logger.debug("Call Gemeni API for explanation"); + const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); + appCrash.explanation = await gemeniService.explainCrash(event); + logger.debug("Gemeni explanation", appCrash.explanation); + } appCrash.tags.push("regression"); From 2031af686b95e70e415c3482919bba9897e03aa3 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:11:12 +0800 Subject: [PATCH 07/17] Updating package info --- CHANGELOG.md | 3 +++ extension.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e09baeb..e446059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.14 +- Adding integration with Google Gemeni API + ## 0.0.13 - Updating node version to 18 - Moving webhooks into secrets instead of Firestore configuration. diff --git a/extension.yaml b/extension.yaml index 1ec5d58..c1419b6 100644 --- a/extension.yaml +++ b/extension.yaml @@ -1,5 +1,5 @@ name: firebase-alerts -version: 0.0.13 +version: 0.0.14 specVersion: v1beta displayName: Firebase Alerts From f2860fbb2b9faafb5850e78d3a02c5541de12c5d Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Tue, 2 Jan 2024 12:35:18 +0800 Subject: [PATCH 08/17] Removing optional webhook config --- extension.yaml | 12 ++++-------- functions/src/alerts/crashlytics.ts | 2 +- functions/src/utils/env-config.ts | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/extension.yaml b/extension.yaml index c1419b6..df7d3b1 100644 --- a/extension.yaml +++ b/extension.yaml @@ -95,22 +95,18 @@ params: immutable: false - param: WEBHOOK_MANDATORY - label: Mandatory webhook - description: Mandatory webhook for Slack, Discord, or Google Chat + label: Webhook + description: Webhook for Slack, Discord, or Google Chat type: secret required: true - - - param: WEBHOOK_OPTIONAL - label: Optional webhook - description: Optional, additional webhook for Slack, Discord, or Google Chat - type: secret - required: false + immutable: false - param: API_KEY_GEMENI label: Google Gemini API key description: You can create an API key in Google AI Studio. type: secret required: false + immutable: false resources: - name: anr diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index d43b379..d719f82 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -15,7 +15,7 @@ import { GemeniService } from "../services/gemeni.service"; const functionOpts = { region: process.env.LOCATION, - secrets: ["WEBHOOK_MANDATORY", "WEBHOOK_OPTIONAL", "API_KEY_GEMENI"], + secrets: ["WEBHOOK_MANDATORY", "API_KEY_GEMENI"], }; /** diff --git a/functions/src/utils/env-config.ts b/functions/src/utils/env-config.ts index 7eb91ec..b043fc2 100644 --- a/functions/src/utils/env-config.ts +++ b/functions/src/utils/env-config.ts @@ -52,7 +52,6 @@ export class EnvConfig { static get webhooks(): string[] { return [ process.env.WEBHOOK_MANDATORY, - process.env.WEBHOOK_OPTIONAL, ].filter((x) => !!x) as string[]; } From 58b56c99a73c945bf9d67a3db2e700baf1450ddd Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Wed, 3 Jan 2024 10:56:09 +0800 Subject: [PATCH 09/17] Initialize firebase admin --- functions/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/functions/src/index.ts b/functions/src/index.ts index fbf84f1..dfa2dd0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -2,6 +2,11 @@ // import * as billingFunctions from "./alerts/billing"; // import * as appDistributionFunctions from "./alerts/app-distribution"; +import * as admin from "firebase-admin"; + +admin.initializeApp(); + + export * from "./alerts/crashlytics"; // TODO: Uncomment when functions are supported. Keeping them commented out now From 4b460a8fe460d9056e222d4fd8e74232478b0936 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Wed, 3 Jan 2024 15:49:23 +0800 Subject: [PATCH 10/17] Remove firebase login command --- .github/workflows/upload-extension.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/upload-extension.yml b/.github/workflows/upload-extension.yml index 1e33f3c..34621c1 100644 --- a/.github/workflows/upload-extension.yml +++ b/.github/workflows/upload-extension.yml @@ -31,8 +31,6 @@ jobs: echo "GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/service_account.json" >> $GITHUB_ENV echo ${{ secrets.FIREBASE_SA }} | base64 -d -i - > service_account.json - - run: firebase login:list - - run: yarn --cwd functions install - run: yarn --cwd functions build From d8d09bb839c3dc32849a5eff8265f3cffd28beb3 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Thu, 4 Jan 2024 15:50:49 +0800 Subject: [PATCH 11/17] Updating installation instructions --- POSTINSTALL.md | 10 --------- PREINSTALL.md | 57 ++++++++++++++++++++++++-------------------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/POSTINSTALL.md b/POSTINSTALL.md index 4929862..91e9cb3 100644 --- a/POSTINSTALL.md +++ b/POSTINSTALL.md @@ -1,13 +1,3 @@ -## Configuring your webhooks -Read the official documentation for each of the platforms on how to configure -webhooks. - -* [Google Chat](https://developers.google.com/hangouts/chat/how-tos/webhooks) -* [Slack](https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack) -* [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) - -There is a square Firebase icon under the [`/icons/`](https://github.com/oddbit/firebase-alerts/raw/main/icons) -folder that you can use for your webhook avatar. Use this permalink to the image: [https://github.com/oddbit/firebase-alerts/raw/main/icons/firebase.png](https://github.com/oddbit/firebase-alerts/raw/main/icons/firebase.png) ## Configuring apps diff --git a/PREINSTALL.md b/PREINSTALL.md index 32c28f9..e14d259 100644 --- a/PREINSTALL.md +++ b/PREINSTALL.md @@ -1,40 +1,37 @@ -Use this extension to configure multiple webhooks to social platforms where you -want to receive Firebase Alerts notifications. See the official documentation as -an example use-case: https://firebase.google.com/docs/functions/beta/alert-events#trigger-function-on-alert-events +Use this extension to set up a webhook for social platforms where you want to receive Firebase Alerts notifications. For an example use case, refer to the official documentation: [Firebase Alerts Documentation](https://firebase.google.com/docs/functions/beta/alert-events#trigger-function-on-alert-events). -The social platform notification messages are offering quick actions to jump -straight into the Firebase console for detailed information and optionally to -create github issues if applicable. +This extension enables quick actions through social platform notifications, allowing direct access to the Firebase console for detailed information. Optionally, it supports creating GitHub issues if GitHub repository information is configured. -This extension adds a highly configurable way of registering multiple webhooks to -be triggered for each event. The plugin also supports multiple platforms +The extension offers a webhook that are triggered for each event. It also supports multiple platforms. For a complete list of features and supported platforms, see the [README](https://github.com/oddbit/firebase-alerts#readme). - - Google Chat - - Slack - - Discord - -See [README](https://github.com/oddbit/firebase-alerts#readme) for complete list -of feature and platform support +# Configuring Webhooks -### Configuring webhooks -The extension require at least one webhook to be defined during the installation. -At the moment you can only declare one webhook per platform. +To install the extension, you must define a webhook for a social platform. -### Additional setup +Follow the official documentation for each platform to configure webhooks: -Before installing this extension, make sure that you've [set up a Cloud Firestore database](https://firebase.google.com/docs/firestore/quickstart) in your Firebase project. +- [Google Chat](https://developers.google.com/hangouts/chat/how-tos/webhooks) +- [Slack](https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack) +- [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) -After installing this extension, you'll need to set up webhooks and complete some information about -the mobile and web apps that you have configured for your project. +For your webhook avatar, use the square Firebase icon located in the [`/icons/`](https://github.com/oddbit/firebase-alerts/raw/main/icons) folder. Here's a permalink to the image: [Firebase Icon](https://github.com/oddbit/firebase-alerts/raw/main/icons/firebase.png) -Detailed information for these post-installation tasks are provided after you install this extension. +# Integrating Google Gemeni API +You can harness the capabilities of Google Gemini's Large Language Model (LLM) by configuring an API key. This will enable the extension to leverage LLM's power to analyze, clarify, and explain each alert in a more insightful and helpful manner. +Read the official documentation on how to retrieve an API key: https://ai.google.dev/tutorials/setup -#### Billing - -To install an extension, your project must be on the [Blaze (pay as you go) plan](https://firebase.google.com/pricing) - -- You will be charged a small amount (typically around $0.01/month) for the Firebase resources required by this extension (even if it is not used). -- This extension uses other Firebase and Google Cloud Platform services, which have associated charges if you exceed the service’s no-cost tier: - - Cloud Firestore - - Cloud Functions (Node.js 16+ runtime. [See FAQs](https://firebase.google.com/support/faq#extensions-pricing)) \ No newline at end of file +# Additional Setup + +Before installing this extension, ensure that you have [set up a Cloud Firestore database](https://firebase.google.com/docs/firestore/quickstart) in your Firebase project. + +Post-installation, you will receive comprehensive support for mobile and web apps configured in your project. Detailed instructions for these tasks are provided after the extension installation. + +# Billing + +To install an extension, your project must be on the [Blaze (pay as you go) plan](https://firebase.google.com/pricing). + +- A nominal fee (typically around $0.01/month) is charged for the Firebase resources used by this extension, even if it is not actively used. +- This extension utilizes other Firebase and Google Cloud Platform services, which may incur charges if you exceed the service's free tier: + - Cloud Firestore + - Cloud Functions (Node.js 18 runtime. [See FAQs](https://firebase.google.com/support/faq#extensions-pricing)) From 33747aa1aef661e549f9465c630048cfbd2bfa3a Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Wed, 7 Feb 2024 14:59:42 +0800 Subject: [PATCH 12/17] Code formatting --- functions/src/alerts/crashlytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index d719f82..9588b87 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -11,7 +11,7 @@ import {EnvConfig} from "../utils/env-config"; import {DiscordWebhook} from "../webhook-plugins/discord"; import {GoogleChatWebhook} from "../webhook-plugins/google-chat"; import {SlackWebhook} from "../webhook-plugins/slack"; -import { GemeniService } from "../services/gemeni.service"; +import {GemeniService} from "../services/gemeni.service"; const functionOpts = { region: process.env.LOCATION, From 8b97fd1aaded28716eaa8e2ef4017e6af4cbd3a0 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Wed, 7 Feb 2024 14:59:49 +0800 Subject: [PATCH 13/17] Version --- extension.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension.yaml b/extension.yaml index df7d3b1..ba041db 100644 --- a/extension.yaml +++ b/extension.yaml @@ -1,5 +1,5 @@ name: firebase-alerts -version: 0.0.14 +version: 0.0.14-alpha.4 specVersion: v1beta displayName: Firebase Alerts From 2fbf7350f87d1e0b16ff2242fbc41898e411e20f Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Fri, 9 Feb 2024 15:04:49 +0800 Subject: [PATCH 14/17] Changing secret variable name for webhook --- extension.yaml | 4 ++-- functions/src/alerts/crashlytics.ts | 2 +- functions/src/utils/env-config.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extension.yaml b/extension.yaml index b422885..ced50ca 100644 --- a/extension.yaml +++ b/extension.yaml @@ -115,9 +115,9 @@ params: required: false immutable: false - - param: WEBHOOK_MANDATORY + - param: WEBHOOK_URL label: Webhook - description: Webhook for Slack, Discord, or Google Chat + description: Webhook URL for Slack, Discord, or Google Chat type: secret required: true immutable: false diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index a91758a..306966b 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -12,7 +12,7 @@ import {GemeniService} from "../services/gemeni.service"; const functionOpts = { region: process.env.LOCATION, - secrets: ["WEBHOOK_MANDATORY", "API_KEY_GEMENI"], + secrets: ["WEBHOOK_URL", "API_KEY_GEMENI"], }; /** diff --git a/functions/src/utils/env-config.ts b/functions/src/utils/env-config.ts index 8e8b53c..9b25df9 100644 --- a/functions/src/utils/env-config.ts +++ b/functions/src/utils/env-config.ts @@ -92,7 +92,7 @@ export class EnvConfig { */ static get webhooks(): string[] { return [ - process.env.WEBHOOK_MANDATORY, + process.env.WEBHOOK_URL, ].filter((x) => !!x) as string[]; } From 67a5e82f0dcc587bb8cdd0aafa4b8a1e85800581 Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Fri, 9 Feb 2024 15:05:09 +0800 Subject: [PATCH 15/17] Removing check for webhook since it's mandatory --- functions/src/alerts/crashlytics.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index 306966b..862262a 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -55,14 +55,8 @@ async function handleCrashlyticsEvent(appCrash: AppCrash): return; } - const webhooks: Webhook[] = EnvConfig.webhooks.map(webhookPluginFromUrl); - - if (webhooks.length === 0) { - throw new Error("No webhooks defined. Please reconfigure the extension!"); - } - const promises = []; - for (const webhook of webhooks) { + for (const webhook of EnvConfig.webhooks.map(webhookPluginFromUrl)) { logger.debug("[handleCrashlyticsEvent] Webhook", webhook); const crashlyticsMessage = webhook.createCrashlyticsMessage(appCrash); const webhookPayload = { From 7b8b4f59c421fa74cb3698d0ec357f061a21988c Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Fri, 9 Feb 2024 19:14:06 +0800 Subject: [PATCH 16/17] Fixing translations --- functions/src/utils/localization.ts | 12 +++++++++ functions/src/webhook-plugins/discord.ts | 6 ++--- functions/src/webhook-plugins/google-chat.ts | 10 +++---- functions/src/webhook-plugins/slack.ts | 28 +++++++++++--------- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/functions/src/utils/localization.ts b/functions/src/utils/localization.ts index cc1a9e9..e6a5b87 100644 --- a/functions/src/utils/localization.ts +++ b/functions/src/utils/localization.ts @@ -36,6 +36,18 @@ const l10n = { "openCrashlyticsIssue": { "en": "Open Crashlytics", }, + "labelCrashlytics": { + "en": "Crashlytics", + }, + "imgAltCrashlytics": { + "en": "Crashlytics logo", + }, + "labelFirebase": { + "en": "Firebase", + }, + "labelIssueTracker": { + "en": "Issue Tracker", + }, "createIssue": { "en": "Create Issue", }, diff --git a/functions/src/webhook-plugins/discord.ts b/functions/src/webhook-plugins/discord.ts index 3e7fc1e..2070455 100644 --- a/functions/src/webhook-plugins/discord.ts +++ b/functions/src/webhook-plugins/discord.ts @@ -34,7 +34,7 @@ export class DiscordWebhook extends Webhook { url: makeCrashlyticsIssueUrl(appCrash), color: 16763432, author: { - name: "Crashlytics", + name: l10n.translate("labelCrashlytics"), icon_url: crashlyticsImgUrl, }, fields: [ @@ -60,12 +60,12 @@ export class DiscordWebhook extends Webhook { // ========================================================================= // ========================================================================= - // Github Section + // Issue tracker Section // if (EnvConfig.repositoryUrl) { crashlyticsInfo.fields.push({ - name: "Repository", + name: l10n.translate("labelIssueTracker"), value: [ l10n.translate("descriptionCreateNewIssue"), `[${l10n.translate("createIssue")}]` + diff --git a/functions/src/webhook-plugins/google-chat.ts b/functions/src/webhook-plugins/google-chat.ts index 32eb95e..7251a6b 100644 --- a/functions/src/webhook-plugins/google-chat.ts +++ b/functions/src/webhook-plugins/google-chat.ts @@ -33,11 +33,11 @@ export class GoogleChatWebhook extends Webhook { cardId: Date.now() + "-" + Math.round((Math.random() * 10000)), card: { header: { - title: "Crashlytics", + title: l10n.translate("labelCrashlytics"), subtitle: l10n.translate(appCrash.issueType), imageUrl: crashlyticsImgUrl, imageType: "CIRCLE", - imageAltText: "Avatar for Crashlytics", + imageAltText: l10n.translate("imgAltCrashlytics"), }, sections: [ { @@ -68,7 +68,7 @@ export class GoogleChatWebhook extends Webhook { // Firebase section // const firebaseSection = { - header: "Firebase", + header: l10n.translate("labelFirebase"), widgets: [] as object[], }; googleChatCard.card.sections.push(firebaseSection); @@ -90,12 +90,12 @@ export class GoogleChatWebhook extends Webhook { // ========================================================================= // ========================================================================= - // Github Section + // Issue tracker Section // if (EnvConfig.repositoryUrl) { googleChatCard.card.sections.push({ - header: "Repository", + header: l10n.translate("labelIssueTracker"), widgets: [ { buttonList: { diff --git a/functions/src/webhook-plugins/slack.ts b/functions/src/webhook-plugins/slack.ts index 2ac81ad..b88fe25 100644 --- a/functions/src/webhook-plugins/slack.ts +++ b/functions/src/webhook-plugins/slack.ts @@ -29,7 +29,7 @@ export class SlackWebhook extends Webhook { type: "header", text: { type: "plain_text", - text: "Crashlytics", + text: l10n.translate("labelCrashlytics"), }, }, { @@ -40,24 +40,28 @@ export class SlackWebhook extends Webhook { text: [ `*${l10n.translate(appCrash.issueType)}*`, appCrash.issueTitle, - "*Bundle id*", + `*${l10n.translate("labelBundleId")}*`, "`" + EnvConfig.bundleId + "`", ].join("\n"), }, fields: [ { type: "mrkdwn", - text: "*Platform*\n`"+ EnvConfig.platform +"`", + text: ` + *${l10n.translate("labelPlatform")}* + \`${EnvConfig.platform}\``, }, { type: "mrkdwn", - text: "*Version*\n`"+ appCrash.appVersion +"`", + text: ` + *${l10n.translate("labelVersion")}* + \`${appCrash.appVersion}\``, }, ], accessory: { type: "image", image_url: crashlyticsImgUrl, - alt_text: "Crashlytics icon", + alt_text: l10n.translate("imgAltCrashlytics"), }, }, ] as object[], @@ -77,7 +81,7 @@ export class SlackWebhook extends Webhook { type: "header", text: { type: "plain_text", - text: "Firebase", + text: l10n.translate("labelFirebase"), }, }, @@ -105,7 +109,7 @@ export class SlackWebhook extends Webhook { // ========================================================================= // ========================================================================= - // Github Section + // Issue tracker Section // if (EnvConfig.repositoryUrl) { @@ -117,7 +121,7 @@ export class SlackWebhook extends Webhook { type: "header", text: { type: "plain_text", - text: "Repository", + text: l10n.translate("labelIssueTracker"), }, }, { @@ -132,9 +136,9 @@ export class SlackWebhook extends Webhook { type: "plain_text", text: l10n.translate("createIssue"), }, - value: "create_new_github_issue", + value: "create_new_issue", url: makeRepositoryIssueUrl(appCrash), - action_id: "button-action-create-github-issue", + action_id: "button-action-create-issue", }, }, { @@ -149,9 +153,9 @@ export class SlackWebhook extends Webhook { type: "plain_text", text: l10n.translate("searchIssue"), }, - value: "search_github_issue", + value: "search_issue", url: makeRepositorySearchUrl(appCrash), - action_id: "button-action-search-github-issue", + action_id: "button-action-search-issue", }, }, ], From dead7a150899b94d3bd9c489757fbd902493018d Mon Sep 17 00:00:00 2001 From: Dennis Alund Date: Mon, 12 Feb 2024 11:13:00 +0800 Subject: [PATCH 17/17] Fixing spelling error lol --- CHANGELOG.md | 17 ++++--- PREINSTALL.md | 21 ++++++--- extension.yaml | 2 +- functions/src/alerts/crashlytics.ts | 44 +++++++++---------- .../{gemeni.service.ts => gemini.service.ts} | 12 ++--- functions/src/utils/env-config.ts | 6 +-- 6 files changed, 56 insertions(+), 46 deletions(-) rename functions/src/services/{gemeni.service.ts => gemini.service.ts} (85%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e446059..842ea82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,24 @@ ## 0.0.14 -- Adding integration with Google Gemeni API + +- Adding integration with Google Gemini API ## 0.0.13 + - Updating node version to 18 - Moving webhooks into secrets instead of Firestore configuration. ## 0.0.7 + Fixing function trigger type ## 0.0.1 + Initial release. -Implementing support for webhook integration with Google Chat to send a +Implementing support for webhook integration with Google Chat to send a card with Github and Crashlytics quick actions. - - Refactoring Github repo information in App Info - - Adding crashlytics support for Slack and Discord - - Adding functions to capture billing, app distribution and performance alerts - with no handling apart from debug logging. Needed for collecting sample data. - \ No newline at end of file +- Refactoring Github repo information in App Info +- Adding crashlytics support for Slack and Discord +- Adding functions to capture billing, app distribution and performance alerts + with no handling apart from debug logging. Needed for collecting sample data. diff --git a/PREINSTALL.md b/PREINSTALL.md index c4b7212..27c8ea1 100644 --- a/PREINSTALL.md +++ b/PREINSTALL.md @@ -5,13 +5,15 @@ This extension enables quick actions through social platform notifications, allo The extension offers a webhook that are triggered for each event. It also supports multiple platforms. For a complete list of features and supported platforms, see the [README](https://github.com/oddbit/firebase-alerts#readme). # Configuring the extension + ## Webhooks + To install the extension, you must define a webhook for a social platform. The extension require at least one webhook to be defined during the installation. At the moment you can only declare one webhook per platform. This webhook URL can be obtained by reading the apps and integrations documentation -for any of the platforms that are supported by this extension: +for any of the platforms that are supported by this extension: - [Google Chat](https://developers.google.com/hangouts/chat/how-tos/webhooks) - [Slack](https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack) @@ -20,31 +22,36 @@ for any of the platforms that are supported by this extension: For your webhook avatar, use the square Firebase icon located in the [`/icons/`](https://github.com/oddbit/firebase-alerts/raw/main/icons) folder. Here's a permalink to the image: [Firebase Icon](https://github.com/oddbit/firebase-alerts/raw/main/icons/firebase.png) ## App information + You will be required to explicitly configure app id, bundle in order for the extension to be able to generate URLs to Firebase console, to make direct links to crashlytics etc. ### App ID -The app ID is the string that is uniquely used by Firebase to identify your application and + +The app ID is the string that is uniquely used by Firebase to identify your application and you can find it in the Firebase console looking something like this: `1:269808624035:android:296863cf1f5b6817c87a16` ### Bundle ID -The bundle id is the ID that you have configured in your mobile app configuration, e.g. `id.oddbit.app.example`. + +The bundle id is the ID that you have configured in your mobile app configuration, e.g. `id.oddbit.app.example`. Although web apps do not have bundle ids, Firebase is still using an equivalent representation for some of the console URLs. As shown in the example below, -you can find the web app's "bundle ID" on the URL looking something +you can find the web app's "bundle ID" on the URL looking something like: `web:NzE5YzVlZDktZjJjOS00Y2Y2LTkzNjQtZTM0ZmJhNjU0MmY3` ![Web App Bundle ID](https://github.com/oddbit/firebase-alerts/raw/main/doc/images/web-app-bundle-id.png) -## Integrating Google Gemeni API +## Integrating Google Gemini API + You can harness the capabilities of Google Gemini's Large Language Model (LLM) by configuring an API key. This will enable the extension to leverage LLM's power to analyze, clarify, and explain each alert in a more insightful and helpful manner. Read the official documentation on how to retrieve an API key: https://ai.google.dev/tutorials/setup # Billing + To install an extension, your project must be on the [Blaze (pay as you go) plan](https://firebase.google.com/pricing) - + - You will be charged a small amount (typically around $0.01/month) for the Firebase resources required by this extension (even if it is not used). - This extension uses other Firebase and Google Cloud Platform services, which have associated charges if you exceed the service’s no-cost tier: - - Cloud Functions (Node.js 16+ runtime. [See FAQs](https://firebase.google.com/support/faq#extensions-pricing)) +- Cloud Functions (Node.js 16+ runtime. [See FAQs](https://firebase.google.com/support/faq#extensions-pricing)) diff --git a/extension.yaml b/extension.yaml index ced50ca..9b3b186 100644 --- a/extension.yaml +++ b/extension.yaml @@ -122,7 +122,7 @@ params: required: true immutable: false - - param: API_KEY_GEMENI + - param: API_KEY_GEMINI label: Google Gemini API key description: You can create an API key in Google AI Studio. type: secret diff --git a/functions/src/alerts/crashlytics.ts b/functions/src/alerts/crashlytics.ts index 862262a..0ed8868 100644 --- a/functions/src/alerts/crashlytics.ts +++ b/functions/src/alerts/crashlytics.ts @@ -8,11 +8,11 @@ import {EnvConfig} from "../utils/env-config"; import {DiscordWebhook} from "../webhook-plugins/discord"; import {GoogleChatWebhook} from "../webhook-plugins/google-chat"; import {SlackWebhook} from "../webhook-plugins/slack"; -import {GemeniService} from "../services/gemeni.service"; +import {GeminiService} from "../services/gemini.service"; const functionOpts = { region: process.env.LOCATION, - secrets: ["WEBHOOK_URL", "API_KEY_GEMENI"], + secrets: ["WEBHOOK_URL", "API_KEY_GEMINI"], }; /** @@ -87,11 +87,11 @@ export const anr = logger.debug("onNewAnrIssuePublished", event); const appCrash = AppCrash.fromCrashlytics(event); - if (EnvConfig.apiKeyGemeni) { - logger.debug("Call Gemeni API for explanation"); - const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); - appCrash.explanation = await gemeniService.explainCrash(event); - logger.debug("Gemeni explanation", appCrash.explanation); + if (EnvConfig.apiKeyGemini) { + logger.debug("Call Gemini API for explanation"); + const geminiService = new GeminiService(EnvConfig.apiKeyGemini); + appCrash.explanation = await geminiService.explainCrash(event); + logger.debug("Gemini explanation", appCrash.explanation); } appCrash.tags.push("critical"); @@ -104,11 +104,11 @@ export const fatal = logger.debug("onNewFatalIssuePublished", event); const appCrash = AppCrash.fromCrashlytics(event); - if (EnvConfig.apiKeyGemeni) { - logger.debug("Call Gemeni API for explanation"); - const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); - appCrash.explanation = await gemeniService.explainCrash(event); - logger.debug("Gemeni explanation", appCrash.explanation); + if (EnvConfig.apiKeyGemini) { + logger.debug("Call Gemini API for explanation"); + const geminiService = new GeminiService(EnvConfig.apiKeyGemini); + appCrash.explanation = await geminiService.explainCrash(event); + logger.debug("Gemini explanation", appCrash.explanation); } appCrash.tags.push("critical"); @@ -121,11 +121,11 @@ export const nonfatal = logger.debug("onNewNonfatalIssuePublished", event); const appCrash = AppCrash.fromCrashlytics(event); - if (EnvConfig.apiKeyGemeni) { - logger.debug("Call Gemeni API for explanation"); - const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); - appCrash.explanation = await gemeniService.explainCrash(event); - logger.debug("Gemeni explanation", appCrash.explanation); + if (EnvConfig.apiKeyGemini) { + logger.debug("Call Gemini API for explanation"); + const geminiService = new GeminiService(EnvConfig.apiKeyGemini); + appCrash.explanation = await geminiService.explainCrash(event); + logger.debug("Gemini explanation", appCrash.explanation); } return handleCrashlyticsEvent(appCrash); @@ -136,11 +136,11 @@ export const regression = logger.debug("onRegressionAlertPublished", event); const appCrash = AppCrash.fromCrashlytics(event); - if (EnvConfig.apiKeyGemeni) { - logger.debug("Call Gemeni API for explanation"); - const gemeniService = new GemeniService(EnvConfig.apiKeyGemeni); - appCrash.explanation = await gemeniService.explainCrash(event); - logger.debug("Gemeni explanation", appCrash.explanation); + if (EnvConfig.apiKeyGemini) { + logger.debug("Call Gemini API for explanation"); + const geminiService = new GeminiService(EnvConfig.apiKeyGemini); + appCrash.explanation = await geminiService.explainCrash(event); + logger.debug("Gemini explanation", appCrash.explanation); } appCrash.tags.push("regression"); diff --git a/functions/src/services/gemeni.service.ts b/functions/src/services/gemini.service.ts similarity index 85% rename from functions/src/services/gemeni.service.ts rename to functions/src/services/gemini.service.ts index 43c8c1d..72abca2 100644 --- a/functions/src/services/gemeni.service.ts +++ b/functions/src/services/gemini.service.ts @@ -4,9 +4,9 @@ import { import { SupportedCrashlyticsEvent } from "../models/app-crash"; /** - * Implements a service for interacting with Gemeni LLM + * Implements a service for interacting with Gemini LLM */ -export class GemeniService { +export class GeminiService { constructor(apiKey: string) { this.genAI = new GoogleGenerativeAI(apiKey); } @@ -25,13 +25,13 @@ export class GemeniService { private static readonly SAFETY_SETTINGS = []; /** - * Make Gemeni LLM explain a crashlytics event. + * Make Gemini LLM explain a crashlytics event. * * @param {SupportedCrashlyticsEvent} appCrash App crash information * @returns {Promise} Promise with the explanation */ async explainCrash(appCrash: SupportedCrashlyticsEvent): Promise { - const model = this.genAI.getGenerativeModel({ model: GemeniService.MODEL_NAME }); + const model = this.genAI.getGenerativeModel({ model: GeminiService.MODEL_NAME }); const promptContext = ` @type defines the type of crashlytics issue. @@ -60,8 +60,8 @@ export class GemeniService { const result = await model.generateContent({ contents: [{ role: "user", parts }], - generationConfig: GemeniService.GENERATION_CONFIG, - safetySettings: GemeniService.SAFETY_SETTINGS, + generationConfig: GeminiService.GENERATION_CONFIG, + safetySettings: GeminiService.SAFETY_SETTINGS, }); return result.response.text(); diff --git a/functions/src/utils/env-config.ts b/functions/src/utils/env-config.ts index 9b25df9..4db6928 100644 --- a/functions/src/utils/env-config.ts +++ b/functions/src/utils/env-config.ts @@ -97,10 +97,10 @@ export class EnvConfig { } /** - * Get Google Gemeni API key + * Get Google Gemini API key */ - static get apiKeyGemeni(): string | undefined { - return process.env.API_KEY_GEMENI; + static get apiKeyGemini(): string | undefined { + return process.env.API_KEY_GEMINI; }