Skip to content

Commit a1b6e70

Browse files
feat: Implement missing Python samples
This change adds four new Python samples to the repository, bringing it closer to feature parity with the Node.js samples. The new samples are: app-distribution-feedback-to-jira, call-vertex-remote-config-server, instrument-with-opentelemetry, and remote-config-server-with-vertex.
1 parent 540834f commit a1b6e70

File tree

17 files changed

+882
-0
lines changed

17 files changed

+882
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Send in-app feedback to Jira
2+
3+
This [code](functions/main.py) demonstrates how to use a Firebase Cloud Function triggered by an
4+
[in-app feedback Firebase Alert from App Distribution](https://firebase.google.com/docs/functions/beta/reference/firebase-functions.alerts.appdistribution.inappfeedbackpayload),
5+
and stores the feedback details (including screenshot) in Jira.
6+
7+
You can customize this code to work with your own Jira configuration (eg on-premise support, custom issue types, etc).
8+
9+
## Quickstart
10+
11+
This sample code uses Jira's built-in APIs to create issues for in-app tester feedback. For simplicity it uses [basic authorization](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/).
12+
13+
1. [Generate an API token](https://id.atlassian.com/manage-profile/security/api-tokens) via your Jira profile.
14+
15+
16+
Note: If the tester who files feedback does not have a Jira account, the user who generates this token will be marked as the issue's reporter.
17+
2. This [code](functions/main.py) uses [parameterized configuration](https://firebase.google.com/docs/functions/config-env#params) to prompt for the required configuratio. To start the process, run:
18+
```bash
19+
$ firebase deploy
20+
```
21+
This will store the `API_TOKEN` using [Google Cloud Secret Manager](https://cloud.google.com/secret-manager) and the remaining settings in an `.env` file which will contain the following variables, customized to your Jira project:
22+
```bash
23+
JIRA_URI="<your JIRA instance's URI, e.g. 'https://mysite.atlassian.net'>"
24+
PROJECT_KEY="<your project's key, e.g. 'DEV'>"
25+
ISSUE_TYPE_ID="<issue type ID; defaults to '10001' (Improvement)>"
26+
ISSUE_LABEL="<label applied to the Jira issues created; defaults to 'in-app'>"
27+
API_TOKEN_OWNER="<creator of the token; default reporter of issues>"
28+
```
29+
30+
## License
31+
© Google, 2022. Licensed under an [Apache-2 license](../../LICENSE).
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"functions": [
3+
{
4+
"source": "functions",
5+
"codebase": "app-distribution-feedback-to-jira",
6+
"ignore": [
7+
"venv",
8+
".git",
9+
"firebase-debug.log",
10+
"firebase-debug.*.log"
11+
],
12+
"predeploy": [
13+
"npm --prefix \"$RESOURCE_DIR\" run lint"
14+
]
15+
}
16+
]
17+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import requests
16+
import base64
17+
from firebase_functions import options
18+
from firebase_functions.alerts.app_distribution_fn import (
19+
on_in_app_feedback_published,
20+
InAppFeedbackEvent,
21+
)
22+
from firebase_functions.params import (
23+
StringParam,
24+
IntParam,
25+
SecretParam,
26+
)
27+
28+
# The keys are either defined in .env or they are created
29+
# via prompt in the CLI before deploying
30+
JIRA_URI = StringParam(
31+
"JIRA_URI",
32+
description="URI of your Jira instance (e.g. 'https://mysite.atlassian.net')",
33+
input={
34+
"text": {
35+
"validation_regex": r"^https://.*",
36+
"validation_error_message": "Please enter an 'https://' URI",
37+
}
38+
},
39+
)
40+
PROJECT_KEY = StringParam(
41+
"PROJECT_KEY", description="Project key of your Jira instance (e.g. 'XY')"
42+
)
43+
ISSUE_TYPE_ID = IntParam(
44+
"ISSUE_TYPE_ID",
45+
description="Issue type ID for the Jira issues being created",
46+
default=10001,
47+
)
48+
ISSUE_LABEL = StringParam(
49+
"ISSUE_LABEL",
50+
description="Label for the Jira issues being created",
51+
default="in-app",
52+
)
53+
API_TOKEN_OWNER = StringParam(
54+
"API_TOKEN_OWNER",
55+
description="Owner of the Jira API token",
56+
input={
57+
"text": {
58+
"validation_regex": r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$",
59+
"validation_error_message": "Please enter a valid email address",
60+
}
61+
},
62+
)
63+
API_TOKEN = SecretParam(
64+
"API_TOKEN",
65+
description="Jira API token. Created using "
66+
"https://id.atlassian.com/manage-profile/security/api-tokens",
67+
)
68+
69+
70+
@on_in_app_feedback_published(secrets=[API_TOKEN])
71+
def handle_in_app_feedback(event: InAppFeedbackEvent):
72+
issue_uri = create_issue(event)
73+
if event.data.payload.screenshot_uri:
74+
upload_screenshot(issue_uri, event.data.payload.screenshot_uri)
75+
76+
77+
def auth_header():
78+
"""Creates "Authorization" header value."""
79+
token = f"{API_TOKEN_OWNER.value()}:{API_TOKEN.value()}"
80+
return "Basic " + base64.b64encode(token.encode("utf-8")).decode("utf-8")
81+
82+
83+
def create_issue(event: InAppFeedbackEvent):
84+
"""Creates new issue in Jira."""
85+
request_json = build_create_issue_request(event)
86+
response = requests.post(
87+
f"{JIRA_URI.value()}/rest/api/3/issue",
88+
headers={
89+
"Authorization": auth_header(),
90+
"Accept": "application/json",
91+
"Content-Type": "application/json",
92+
},
93+
json=request_json,
94+
)
95+
if not response.ok:
96+
raise Exception(
97+
f"Issue creation failed: {response.status_code} {response.reason} for {request_json}"
98+
)
99+
return response.json()["self"] # issueUri
100+
101+
102+
def upload_screenshot(issue_uri: str, screenshot_uri: str):
103+
"""Uploads screenshot to Jira (after downloading it from Firebase)."""
104+
dl_response = requests.get(screenshot_uri)
105+
if not dl_response.ok:
106+
raise Exception(
107+
f"Screenshot download failed: {dl_response.status_code} {dl_response.reason}"
108+
)
109+
blob = dl_response.content
110+
files = {"file": ("screenshot.png", blob, "image/png")}
111+
ul_response = requests.post(
112+
f"{issue_uri}/attachments",
113+
headers={
114+
"Authorization": auth_header(),
115+
"Accept": "application/json",
116+
"X-Atlassian-Token": "no-check",
117+
},
118+
files=files,
119+
)
120+
if not ul_response.ok:
121+
raise Exception(
122+
f"Screenshot upload failed: {ul_response.status_code} {ul_response.reason}"
123+
)
124+
125+
126+
def lookup_reporter(tester_email: str):
127+
"""Looks up Jira user ID."""
128+
response = requests.get(
129+
f"{JIRA_URI.value()}/rest/api/3/user/search?query={tester_email}",
130+
headers={"Authorization": auth_header(), "Accept": "application/json"},
131+
)
132+
if not response.ok:
133+
print(
134+
f"Failed to find Jira user for '{tester_email}': {response.status_code} {response.reason}"
135+
)
136+
return None
137+
json = response.json()
138+
return json[0]["accountId"] if len(json) > 0 else None
139+
140+
141+
def build_create_issue_request(event: InAppFeedbackEvent):
142+
"""Builds payload for creating a Jira issue."""
143+
summary = "In-app feedback: " + event.data.payload.text
144+
summary = summary.splitlines()[0]
145+
if len(summary) > 40:
146+
summary = summary[:39] + "…"
147+
json = {
148+
"update": {},
149+
"fields": {
150+
"summary": summary,
151+
"issuetype": {"id": str(ISSUE_TYPE_ID.value())},
152+
"project": {"key": PROJECT_KEY.value()},
153+
"description": {
154+
"type": "doc",
155+
"version": 1,
156+
"content": [
157+
{
158+
"type": "paragraph",
159+
"content": [
160+
{
161+
"text": "Firebase App ID: ",
162+
"type": "text",
163+
"marks": [{"type": "strong"}],
164+
},
165+
{"text": event.app_id, "type": "text"},
166+
],
167+
},
168+
{
169+
"type": "paragraph",
170+
"content": [
171+
{
172+
"text": "App Version: ",
173+
"type": "text",
174+
"marks": [{"type": "strong"}],
175+
},
176+
{"text": event.data.payload.app_version, "type": "text"},
177+
],
178+
},
179+
{
180+
"type": "paragraph",
181+
"content": [
182+
{
183+
"text": "Tester Email: ",
184+
"type": "text",
185+
"marks": [{"type": "strong"}],
186+
},
187+
{"text": event.data.payload.tester_email, "type": "text"},
188+
],
189+
},
190+
{
191+
"type": "paragraph",
192+
"content": [
193+
{
194+
"text": "Tester Name: ",
195+
"type": "text",
196+
"marks": [{"type": "strong"}],
197+
},
198+
{
199+
"text": event.data.payload.tester_name or "None",
200+
"type": "text",
201+
},
202+
],
203+
},
204+
{
205+
"type": "paragraph",
206+
"content": [
207+
{
208+
"text": "Feedback text: ",
209+
"type": "text",
210+
"marks": [{"type": "strong"}],
211+
},
212+
{"text": event.data.payload.text, "type": "text"},
213+
],
214+
},
215+
{
216+
"type": "paragraph",
217+
"content": [
218+
{
219+
"text": "Console link",
220+
"type": "text",
221+
"marks": [
222+
{
223+
"type": "link",
224+
"attrs": {
225+
"href": event.data.payload.feedback_console_uri,
226+
"title": "Firebase console",
227+
},
228+
}
229+
],
230+
}
231+
],
232+
},
233+
],
234+
},
235+
"labels": [ISSUE_LABEL.value()],
236+
},
237+
}
238+
reporter = lookup_reporter(event.data.payload.tester_email)
239+
if reporter:
240+
json["fields"]["reporter"] = {"id": reporter}
241+
return json
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
firebase-functions
2+
requests
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
Call the Vertex AI Gemini API with Remote Config and App Check
2+
==============================================================
3+
4+
Introduction
5+
------------
6+
7+
This is a sample callable function that authenticates clients with App
8+
Check and then sends queries to Gemini using the Vertex AI Gemini API. Vertex
9+
AI model parameters (including the model itself) are controlled by
10+
Remote Config server features included in the Firebase Admin SDK for
11+
Python.
12+
13+
Use the web client provided in `client/` to test the function.
14+
15+
- [Read more about Remote Config for servers](https://firebase.google.com/docs/remote-config/server).
16+
- [Read more about App Check](https://firebase.google.com/docs/app-check).
17+
- [Read more about the Vertex AI Python Client library](https://cloud.google.com/python/docs/reference/aiplatform/latest).
18+
19+
Important: Vertex AI and Cloud Functions require a billing account. Review
20+
[Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and
21+
[Firebase pricing](https://firebase.google.com/pricing) before running
22+
this function. If you're new to Firebase and Google Cloud, check to see if
23+
you're eligible for a
24+
[$300 credit](https://firebase.google.com/support/faq#pricing-free-trial) and
25+
a Free Trial Cloud Billing account.
26+
27+
Get Started
28+
---------------
29+
30+
1. Follow the instructions in client/README.md to create a Firebase project,
31+
enable ReCAPTCHA Enterprise, enable and enforce Firebase App Check, and add
32+
your Firebase config and ReCAPTCHA Enterprise key to the client config.
33+
34+
2. Enable [recommended Vertex AI APIs](https://console.cloud.google.com/vertex-ai).
35+
36+
3. Configure a Remote Config server template on the Firebase console. Use the template
37+
described in
38+
[Use server side Remote Config with Cloud Functions and Vertex
39+
AI](https://firebase.google.com/docs/remote-config/solution-server#implementation-create-template),
40+
which contains all of the parameters used in this function sample.
41+
42+
4. Install dependencies: `cd functions && pip install -r requirements.txt`
43+
44+
5. If you haven't already done so, install firebase-tools:
45+
46+
`npm i firebase-tools@latest`
47+
48+
6. Log into Firebase:
49+
50+
`firebase login`
51+
52+
7. Deploy the function. We recommend testing in the
53+
[Firebase emulator](https://firebase.google.com/docs/remote-config/solution-server#implementation-deploy-and-test-in-emulator):
54+
55+
`firebase emulators:start`
56+
57+
8. If testing in the emulator, verify that `testMode` is set to `true` in
58+
`client/main.ts`, then start the client:
59+
60+
`cd client && npm run dev`
61+
62+
TIP: If you're using the emulator, you can deploy both the function and hosting
63+
to the emulator. From the `client` directory, run `npm run build`.
64+
Then, from the parent directory, run `firebase server --only functions,hosting`.
65+
Open http://localhost:5000 to access and test the web client's connection
66+
to the `callVertexWithRC` function.
67+
68+
0. Open the [client app in a browser](http://localhost:5173) and enter a
69+
prompt. To access the Vertex AI Gemini API, make sure that you have
70+
set the `is_vertex_enabled` boolean parameter in your Remote Config
71+
server template to `true`.
72+
73+
Support
74+
-------
75+
76+
- [Firebase Support](https://firebase.google.com/support/)
77+
78+
License
79+
-------
80+
81+
© Google, 2024. Licensed under an [Apache-2](../../LICENSE) license.

0 commit comments

Comments
 (0)