Skip to content

Commit b415506

Browse files
authored
feat: Add Apps Script Incident Response app sample (#358)
* feat: Add Apps Script Incident Response app sample using app authentication model.
1 parent 0394995 commit b415506

File tree

11 files changed

+1086
-0
lines changed

11 files changed

+1086
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// [START chat_incident_response_app]
17+
18+
/**
19+
* Responds to a MESSAGE event in Google Chat.
20+
*
21+
* This app only responds to a slash command with the ID 1 ("/closeIncident").
22+
* It will respond to any other message with a simple "Hello" text message.
23+
*
24+
* @param {Object} event the event object from Google Chat
25+
*/
26+
function onMessage(event) {
27+
if (event.message.slashCommand) {
28+
return processSlashCommand_(event);
29+
}
30+
return { "text": "Hello from Incident Response app!" };
31+
}
32+
33+
/**
34+
* Responds to a CARD_CLICKED event in Google Chat.
35+
*
36+
* This app only responds to one kind of dialog (Close Incident).
37+
*
38+
* @param {Object} event the event object from Google Chat
39+
*/
40+
function onCardClick(event) {
41+
if (event.isDialogEvent) {
42+
if (event.dialogEventType == 'SUBMIT_DIALOG') {
43+
return processSubmitDialog_(event);
44+
}
45+
return {
46+
actionResponse: {
47+
type: "DIALOG",
48+
dialogAction: {
49+
actionStatus: "OK"
50+
}
51+
}
52+
};
53+
}
54+
}
55+
56+
/**
57+
* Responds to a MESSAGE event with a Slash command in Google Chat.
58+
*
59+
* This app only responds to a slash command with the ID 1 ("/closeIncident")
60+
* by returning a Dialog.
61+
*
62+
* @param {Object} event the event object from Google Chat
63+
*/
64+
function processSlashCommand_(event) {
65+
if (event.message.slashCommand.commandId != CLOSE_INCIDENT_COMMAND_ID) {
66+
return {
67+
"text": "Command not recognized. Use the command `/closeIncident` to close the incident managed by this space."
68+
};
69+
}
70+
const sections = [
71+
{
72+
header: "Close Incident",
73+
widgets: [
74+
{
75+
textInput: {
76+
label: "Please describe the incident resolution",
77+
type: "MULTIPLE_LINE",
78+
name: "description"
79+
}
80+
},
81+
{
82+
buttonList: {
83+
buttons: [
84+
{
85+
text: "Close Incident",
86+
onClick: {
87+
action: {
88+
function: "closeIncident"
89+
}
90+
}
91+
}
92+
]
93+
}
94+
}
95+
]
96+
}
97+
];
98+
return {
99+
actionResponse: {
100+
type: "DIALOG",
101+
dialogAction: {
102+
dialog: {
103+
body: {
104+
sections,
105+
}
106+
}
107+
}
108+
}
109+
};
110+
}
111+
112+
/**
113+
* Responds to a CARD_CLICKED event with a Dialog submission in Google Chat.
114+
*
115+
* This app only responds to one kind of dialog (Close Incident).
116+
* It creates a Doc with a summary of the incident information and posts a message
117+
* to the space with a link to the Doc.
118+
*
119+
* @param {Object} event the event object from Google Chat
120+
*/
121+
function processSubmitDialog_(event) {
122+
const resolution = event.common.formInputs.description[""].stringInputs.value[0];
123+
const chatHistory = concatenateAllSpaceMessages_(event.space.name);
124+
const chatSummary = summarizeChatHistory_(chatHistory);
125+
const docUrl = createDoc_(event.space.displayName, resolution, chatHistory, chatSummary);
126+
return {
127+
actionResponse: {
128+
type: "NEW_MESSAGE",
129+
},
130+
text: `Incident closed with the following resolution: ${resolution}\n\nHere is the automatically generated post-mortem:\n${docUrl}`
131+
};
132+
}
133+
134+
/**
135+
* Lists all the messages in the Chat space, then concatenate all of them into
136+
* a single text containing the full Chat history.
137+
*
138+
* For simplicity for this demo, it only fetches the first 100 messages.
139+
*
140+
* Messages with slash commands are filtered out, so the returned history will
141+
* contain only the conversations between users and not app command invocations.
142+
*
143+
* @return {string} a text containing all the messages in the space in the format:
144+
* Sender's name: Message
145+
*/
146+
function concatenateAllSpaceMessages_(spaceName) {
147+
// Call Chat API method spaces.messages.list
148+
const response = Chat.Spaces.Messages.list(spaceName, { 'pageSize': 100 });
149+
const messages = response.messages;
150+
// Fetch the display names of the message senders and returns a text
151+
// concatenating all the messages.
152+
let userMap = new Map();
153+
return messages
154+
.filter(message => message.slashCommand === undefined)
155+
.map(message => `${getUserDisplayName_(userMap, message.sender.name)}: ${message.text}`)
156+
.join('\n');
157+
}
158+
159+
/**
160+
* Obtains the display name of a user by using the Admin Directory API.
161+
*
162+
* The fetched display name is cached in the provided map, so we only call the API
163+
* once per user.
164+
*
165+
* If the user does not have a display name, then the full name is used.
166+
*
167+
* @param {Map} userMap a map containing the display names previously fetched
168+
* @param {string} userName the resource name of the user
169+
* @return {string} the user's display name
170+
*/
171+
function getUserDisplayName_(userMap, userName) {
172+
if (userMap.has(userName)) {
173+
return userMap.get(userName);
174+
}
175+
let displayName = 'Unknown User';
176+
try {
177+
const user = AdminDirectory.Users.get(
178+
userName.replace("users/", ""),
179+
{ projection: 'BASIC', viewType: 'domain_public' });
180+
displayName = user.name.displayName ? user.name.displayName : user.name.fullName;
181+
} catch (e) {
182+
// Ignore error if the API call fails (for example, because it's an
183+
// out-of-domain user or Chat app)) and just use 'Unknown User'.
184+
}
185+
userMap.set(userName, displayName);
186+
return displayName;
187+
}
188+
// [END chat_incident_response_app]
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// [START handle_incident_with_application_credentials]
17+
18+
/**
19+
* Handles an incident by creating a chat space, adding members, and posting a message.
20+
* All the actions are done using application credentials.
21+
*
22+
* @param {Object} formData - The data submitted by the user. It should contain the fields:
23+
* - title: The display name of the chat space.
24+
* - description: The description of the incident.
25+
* - users: A comma-separated string of user emails to be added to the space.
26+
* @return {string} The resource name of the new space.
27+
*/
28+
function handleIncident(formData) {
29+
const users = formData.users.trim().length > 0 ? formData.users.split(',') : [];
30+
const service = getService_();
31+
if (!service.hasAccess()) {
32+
console.error(service.getLastError());
33+
return;
34+
}
35+
const spaceName = createChatSpace_(formData.title, service);
36+
createHumanMembership_(spaceName, getUserEmail(), service);
37+
for (const user of users ){
38+
createHumanMembership_(spaceName, user, service);
39+
}
40+
createMessage_(spaceName, formData.description, service);
41+
return spaceName;
42+
}
43+
/**
44+
* Creates a chat space with application credentials.
45+
*
46+
* @param {string} displayName - The name of the chat space.
47+
* @param {object} service - The credentials of the service account.
48+
* @returns {string} The resource name of the new space.
49+
*/
50+
function createChatSpace_(displayName, service) {
51+
try {
52+
// For private apps, the alias can be used
53+
const my_customer_alias = "customers/my_customer";
54+
// Specify the space to create.
55+
const space = {
56+
displayName: displayName,
57+
spaceType: 'SPACE',
58+
customer: my_customer_alias
59+
};
60+
// Call Chat API with a service account to create a message.
61+
const createdSpace = Chat.Spaces.create(
62+
space,
63+
{},
64+
// Authenticate with the service account token.
65+
{'Authorization': 'Bearer ' + service.getAccessToken()});
66+
return createdSpace.name;
67+
} catch (err) {
68+
// TODO (developer) - Handle exception.
69+
console.log('Failed to create space with error %s', err.message);
70+
}
71+
}
72+
/*
73+
* Creates a chat message with application credentials.
74+
*
75+
* @param {string} spaceName - The resource name of the space.
76+
* @param {string} message - The text to be posted.
77+
* @param {object} service - The credentials of the service account.
78+
* @return {string} the resource name of the new space.
79+
*/
80+
function createMessage_(spaceName, message, service) {
81+
try {
82+
// Call Chat API with a service account to create a message.
83+
const result = Chat.Spaces.Messages.create(
84+
{'text': message},
85+
spaceName,
86+
{},
87+
// Authenticate with the service account token.
88+
{'Authorization': 'Bearer ' + service.getAccessToken()});
89+
90+
} catch (err) {
91+
// TODO (developer) - Handle exception.
92+
console.log('Failed to create message with error %s', err.message);
93+
}
94+
}
95+
/**
96+
* Creates a human membership in a chat space with application credentials.
97+
*
98+
* @param {string} spaceName - The resource name of the space.
99+
* @param {string} email - The email of the user to be added.
100+
* @param {object} service - The credentials of the service account.
101+
*/
102+
function createHumanMembership_(spaceName, email, service){
103+
try{
104+
const membership = {
105+
member: {
106+
name: 'users/'+email,
107+
// User type for the membership
108+
type: 'HUMAN'
109+
}
110+
};
111+
const result = Chat.Spaces.Members.create(
112+
membership,
113+
spaceName,
114+
{},
115+
{'Authorization': 'Bearer ' + service.getAccessToken()}
116+
);
117+
} catch (err){
118+
console.log('Failed to create membership with error %s', err.message)
119+
}
120+
121+
}
122+
123+
/*
124+
* Creates a service for the service account.
125+
* @return {object} - The credentials of the service account.
126+
*/
127+
function getService_() {
128+
return OAuth2.createService(APP_CREDENTIALS.client_email)
129+
.setTokenUrl('https://oauth2.googleapis.com/token')
130+
.setPrivateKey(APP_CREDENTIALS.private_key)
131+
.setIssuer(APP_CREDENTIALS.client_email)
132+
.setSubject(APP_CREDENTIALS.client_email)
133+
.setScope(APP_CREDENTIALS_SCOPES)
134+
.setPropertyStore(PropertiesService.getScriptProperties());
135+
}
136+
// [END handle_incident_with_application_credentials]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// [START chat_incident_response_consts]
17+
18+
const PROJECT_ID = 'replace-with-your-project-id';
19+
const CLOSE_INCIDENT_COMMAND_ID = 1;
20+
const APP_CREDENTIALS = 'replace-with-your-app-credentials';
21+
const APP_CREDENTIALS_SCOPES = 'https://www.googleapis.com/auth/chat.bot https://www.googleapis.com/auth/chat.app.memberships https://www.googleapis.com/auth/chat.app.spaces.create';
22+
const VERTEX_AI_LOCATION_ID = 'us-central1';
23+
const MODEL_ID = 'gemini-1.5-flash-002';
24+
// [END chat_incident_response_consts]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// [START chat_incident_response_docs]
17+
18+
/**
19+
* Creates a Doc in the user's Google Drive and writes a summary of the incident information to it.
20+
*
21+
* @param {string} title The title of the incident
22+
* @param {string} resolution Incident resolution described by the user
23+
* @param {string} chatHistory The whole Chat history be included in the document
24+
* @param {string} chatSummary A summary of the Chat conversation to be included in the document
25+
* @return {string} the URL of the created Doc
26+
*/
27+
function createDoc_(title, resolution, chatHistory, chatSummary) {
28+
let doc = DocumentApp.create(title);
29+
let body = doc.getBody();
30+
body.appendParagraph(`Post-Mortem: ${title}`).setHeading(DocumentApp.ParagraphHeading.TITLE);
31+
body.appendParagraph("Resolution").setHeading(DocumentApp.ParagraphHeading.HEADING1);
32+
body.appendParagraph(resolution);
33+
body.appendParagraph("Summary of the conversation").setHeading(DocumentApp.ParagraphHeading.HEADING1);
34+
body.appendParagraph(chatSummary);
35+
body.appendParagraph("Full Chat history").setHeading(DocumentApp.ParagraphHeading.HEADING1);
36+
body.appendParagraph(chatHistory);
37+
return doc.getUrl();
38+
}
39+
// [END chat_incident_response_docs]

0 commit comments

Comments
 (0)