In this article, I’ll tell you about a very simple way to use Google Sheet+Google AppScript to collect data from a webhook. You can also dive into the code of this spreadsheet that I made just for you :*
Recently I wanted to use Google Spreadsheet to log support ticket handling and resolution time.
We use Talkus, combined with Slack in our workflow:
/talkus_new_ticket help pls
and a temporary channel is created (usually #z-silly-name
)** Warning - already made tools are available **
It is worth noting that Talkus offers a reporting feature, but because all our Talkus users are from the same Toucan slack team, the handling times were wrong (the person asking for help was counted as the person responding).
Also, Zapier has a recipe just for webhook and Google Sheets (but where’s the fun in that :p)
I figured I would need :
Fairly simple :) First, I need to check how each piece works, then I’ll put the bricks together.
From the docs : https://developers.google.com/apps-script/guides/sheets#writing_data
Tools > Script Editor...
In the script editor, I added a very simple function, to see if everything works as expected.
function testRun() {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(['some', 'fake', 'data']);
}
Run > Run function... > testRun
Google Script will ask you to authorize the script to write on the spreadsheet, do so.
Tadaa… it worked, a row was added!
That was easy! Now let’s turn this script into a Web App so it can handle requests.
From the docs, we see Google Web Apps have the ability to answer to POST requests. RTFM :
doPost
methodA Web app can handle GET and POST requests using a doPost
and a doGet
method: Let’s try this with very simple methods:
function doPost(e) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(['post test']);
}
function doGet(e) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(['get test']);
}
In the script editor, go to Publish > Deploy as Web App
I chose :
Go to Troubleshooting below if you don’t see the “Anyone, even anonymous” option.
After clicking the deploy button, I was provided with a script url : https://script.google.com/macros/s/XXXXXX-YYYYYYY/exec
If you access it from your browser, your doGet
function should be executed (try it!).
Though here it seems quite simple, making this POST work was actually what took me the longest. In the Google Documentation, I kept reading about options unavailable to me. First I brushed it off as lost in translation (the doc was in English, my interface was in French). Turns out, I was using an enterprise Google App account, and was logged in to multiple accounts at the same time each causing their own set of problems :). It took me some time to figure out, so I’ve added a Troubleshooting section.
I want to see how my app reacts to POST. So I do a simple CURL post :
curl -X POST https://script.google.com/macros/s/xxx-YYY/exec
Let’s add data:
curl -d '{"test":"supertest", "test2":"supertest2"}' -H "Content-Type: application/json" -X POST https://script.google.com/macros/s/xxx-YYY/exec
To test this I modified my code:
function doPost(e) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow([e.postData.contents]);
}
Then I re-published my app:
Be careful here to select the ‘New’ version option in the dropdown.
Note that using the
/dev
URL (found if you follow the latest code link) doesn’t seem to work for POSTs, so for every change you make to your post, you need to publish a new version.
Tadaa! The result is visible in the spreadsheet !
At this point when I was exploring, I got stalled because the permissions of my script and spreadsheet where restricted. This is due to the fact I was doing all this from an enterprise GSuite account. You need to be able to publish documents publicly to anyone in the web for this to work. If you are a Google Admin for the domain, you can enable this. I personally chose to create the spreadsheet from my own consumer account, because I didn’t want to lower the security settings of all our Google Apps.
When trying to access the script from the browser, I was greeted with a page not found error… It turns out since I was logged with multiple google accounts, the URL given was faulty : https://script.google.com/macros/u/0/s/XXXXXX-YYYYYYY/exec
instead of https://script.google.com/macros/s/XXXXXX-YYYYYYY/exec
(cf. issue on StackOverflow)
To fix this, you need to take out the /u/x
part of the URL to test the script, like this : https://script.google.com/macros/s/XXXXXX-YYYYYYY/exec
This generally means the permissions aren’t set right for you to access the said resource.
Here my goal is to use the Google spreadsheet as a database for talkus ticket data.
From the talkus app, in the Integration menu, I just copy-pasted my script url, and pressed the “Send a test message to your webhook” button.
Now I see that the postData content form is this :
Content from talkus webhook test ``` [ { "event": "start", "createdAt": "2017-02-16T16:21:12.895Z", "channelName": "z-denholm-reynholm", "visitorName": "Denholm Reynholm", "visitorId": "AP3qSeAG7d5uTAacH", "identity": { "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit\/527.36 (KHTML, like Gecko) Chrome\/55.0.2883.95 Safari\/537.36", "location": "http:\/\/reynholm.talkus.io\/", "id": "U0AFE3Z12", "name": "den", "visitorId": "AP3qSeAG7d5uTAacH", "ip": "98.189.242.66", "email": "denholm@reynholm.co.uk", "languages": "en", "title": "Talkus Admin" }, "appId": "oKJJXAsT5PprBAQ7W", "messages": [ { "userName": "Denholm Reynholm", "text": "Where are the nice girl of the 5th floor?", "userPicture": "\/\/vignette1.wikia.nocookie.net\/theitcrowd\/images\/0\/0b\/Denholm_declares_war.webp\/revision\/latest?cb=20100425163116", "createdAt": "2017-01-18T15:42:11.000Z" } ], "message": "Denholm Reynholm: Where are the nice girl of the 5th floor?" }, (...) { "event": "end", "createdAt": "2017-02-16T16:49:12.995Z", "channelName": "z-denholm-reynholm", "visitorName": "Denholm Reynholm", "visitorId": "AP3qSeAG7d5uTAacH", "identity": { "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit\/527.36 (KHTML, like Gecko) Chrome\/55.0.2883.95 Safari\/537.36", "location": "http:\/\/reynholm.talkus.io\/", "id": "U0AFE3Z12", "name": "den", "visitorId": "AP3qSeAG7d5uTAacH", "ip": "98.189.242.66", "email": "denholm@reynholm.co.uk", "languages": "en", "title": "Talkus Admin" }, "appId": "oKJJXAsT5PprBAQ7W", "messages": [ { "userName": "Denholm Reynholm", "text": "Where are the nice girl of the 5th floor?", "userPicture": "\/\/vignette1.wikia.nocookie.net\/theitcrowd\/images\/0\/0b\/Denholm_declares_war.webp\/revision\/latest?cb=20100425163116", "createdAt": "2017-01-18T15:42:11.000Z" }, { "userName": "Roy", "text": "At the canteen, at the 4th floor.", "userPicture": "\/\/vignette2.wikia.nocookie.net\/theitcrowd\/images\/9\/9e\/Roy2.webp\/revision\/latest\/scale-to-width-down\/250?cb=20090403154035", "createdAt": "2017-01-18T15:53:23.000Z" } ], "message": "Denholm Reynholm: Where are the nice girl of the 5th floor?\nRoy: At the canteen, at the 4th floor." } ] ```I did some tests by creating tickets in slack and figured I would only need to log end
events, because they contained all relevant info. I would need :
end
event date (createdAt
field)I also eliminated from my search the users with :
userName == 'slackbot'
> Usually automated response to expressions.userPicture == 'https://talkus.io/app/avatar-default.webp'
> The Talkus BotuserName == 'Toucan Tech support'
> Usually automated response from Talkusfunction doPost(e) { | |
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data"); | |
var toLog = handleTalkusEvent(e.postData.contents); | |
if (!toLog) { | |
return; | |
} | |
// Improve this by getting first row column names | |
sheet.appendRow([toLog.endTime, toLog.channelName, toLog.userAsking, toLog.userAskingJoinTime, toLog.firstResponder, toLog.firstResponseTime, toLog.messages]); | |
} | |
// Get only the useful/actionable events and info | |
function handleTalkusEvent(eventString) { | |
var e = JSON.parse(eventString); | |
if (!e) { | |
return; | |
} | |
if (e.length <= 0) { | |
return; | |
} | |
if (e[0].event != 'end') { | |
return; | |
} | |
var usefulInfo = searchMessages(e[0].messages); | |
usefulInfo.channelName = e[0].channelName; | |
usefulInfo.messages = JSON.stringify(e[0].messages); // logging messages for debugging | |
usefulInfo.endTime = e[0].createdAt; | |
return usefulInfo; | |
} | |
function ignoreUser(msg) { | |
return (msg.userPicture == 'https://talkus.io/app/avatar-default.png' || msg.userName == 'slackbot' || msg.userName == 'Toucan Tech support'); | |
} | |
function searchMessages(msgs) { | |
// separate the messages to get | |
// time of first person to get in | |
// time of first responder | |
var userAsking, userAskingJoinTime, firstResponder, firstResponseTime; | |
for (var i=0; i < msgs.length ; i++) { | |
var msg = msgs[i]; | |
if (!userAsking && !ignoreUser(msg)) { | |
userAsking = msg.userName; | |
userAskingJoinTime = msg.createdAt; | |
} | |
if (!!userAsking && !firstResponder && msg.userName != userAsking && !ignoreUser(msg)) { | |
firstResponder = msg.userName; | |
firstResponseTime = msg.createdAt; | |
} | |
} | |
return { | |
userAsking: userAsking, | |
userAskingJoinTime: userAskingJoinTime, | |
firstResponder: firstResponder, | |
firstResponseTime: firstResponseTime | |
} | |
} |
For this part I used our own Toucan application to easily connect to the spreadsheet and visualize the handling time. You can contact our sales rep team for more info, or use some of the free tools available online.
With the data as is, I added a little python / pandas script to compute time deltas, it’s very easy :) Here are the useful lines to convert time<>strings
import pandas as pd
# Dates from Talkus are of the form 2018-02-12T10:55:54.369Z df['userAskingJoinTime'] = pd.to_datetime(df['userAskingJoinTime'], format="%Y-%m-%dT%H:%M:%S", exact=False, errors='coerce')
df['firstResponseTime'] = pd.to_datetime(df['firstResponseTime'], format="%Y-%m-%dT%H:%M:%S", exact=False, errors='coerce')
# Compute difference and convert to seconds df['handlingTime'] = df['firstResponseTime'] - df['userAskingJoinTime']
df['handlingTime'] = df['handlingTime'].astype('timedelta64[s]')
Now for some ridiculous viz :
Why ridiculous? Because the script has been running for a week so there aren’t a lot of data points, hence 1 extra long ticket will break the score. Also tickets created out of business hours have longer handling times, maybe the next iteration of the script could account for that. I’ll do a followup once we have more data :)
What I found here already is that sometimes our tech support team joins a channel but does not write to indicate they are working on the problem: this greatly increases measured handling times. This is coherent with our qualitative analysis of Toucan support workflow which hinted at the need for early communication on an issue, even a simple acknowledgment of the problem. So it sounds like keeping an eye on this metric should lead to interesting results!
Hope this will help, have fun collecting data ;)