Google Apps Scripts, Clasp und Data Studio

Update 29.10.2018Google hat einen neuen Service veröffentlicht, der einiges an syntaktischem Zucker mitbringt, um die Erstellung von Data-Studio-Connectors deutlich zu erleichtern. Die Dokumentation des Services findet sich hier. Dieser Blogpost wurde leicht überarbeitet, um die Änderung zu reflektieren.

//SEIBERT/MEDIA ist nun seit einiger Zeit Google Cloud Partner – und das Projekt kann nicht mehr als Experiment bezeichnet werden: Es ist ein Investitionsbereich. Kein Wunder also, dass an uns als Team Google immer häufiger der Wunsch nach mehr Transparenz herangetragen wird, nach Metriken, nach Messbarkeit.

Das ist nicht nur ein Unternehmenswert, sondern auch wichtig und wertvoll, um verstehen zu können, wie sich das Geschäftsfeld entwickelt und wie wir uns dem anpassen wollen.

Die Amerikaner würden nun sagen:

Enter Google Data Studio.

Data Studio ist ein Produkt von Google, mit dem sich generische Datenreihen modellieren, statistisch auswerten und grafisch darstellen lassen. Heute beschränken wir uns zur Einführung auf ein ganz minimalistisches Beispiel, aber die Möglichkeiten sind nicht enden wollend.

Connectors

Die Anforderung besteht darin, unseren Reseller-Status im Auge behalten zu können. Gut, dazu brauchen wir natürlich erstmal Daten, die wir darstellen möchten. Da wir in unserem Beispiel ganz klein anfangen wollen, soll uns hier die Anzahl der momentan aktiven Kunden und der Trial-Instanzen mit den jeweiligen Benutzerzahlen interessieren.

Für viele andere Datenquellen stellt Google Connectors bereit

Datenquellen definiert man in Data Studio mithilfe sogenannter Connectors. Das kann eine CSV-Datei, aber auch ein anderer Google-Service oder einfach eine externe MySQL-Datenbank sein.

Die Reseller-Konsole ist zwar nicht als eigener Connector verfügbar – aber es ist problemlos möglich, einen Community Connector zu schreiben.

So ein Connector ist ein Google Apps Script, das vier Funktionen mit vordefinierten Namen bereitstellen muss:

  • getConfig() verwendet Data Studio, um dem Benutzer die Konfiguration des Connectors zu ermöglichen.
  • getSchema() erklärt Data Studio, welche Metriken und Dimensionen in einer gegebenen Antwort unseres Connectors zu erwarten sind.
  • getData(request) holt die Daten. Data Studio entscheidet selbständig je nach Konfiguration (und Visualisierung), welche Daten es benötigt. Wir müssen also das request-Objekt respektieren und nur zurückliefern, was Data Studio gerade fordert, sonst gibt es Fehlermeldungen statt Daten.
  • getAuthType() deklariert den Authentifizierungsmodus, den Data Studio mithilfe der Konfiguration aus getConfig() ausführen muss. Innerhalb der meisten Google-Services ist allerdings keine Authentifizierung nötig, auch für unsere Reseller-Konsole nicht.

Clasp

Sehr hilfreich beim Umgang mit Google Apps Scripts ist ein Google-eigenes Tool namens clasp. Es verwaltet GAS-Projekte über die Kommandozeile, wie man das etwa von Git gewohnt ist. Hier gibt es eine Installationsanleitung. Bei der Einrichtung dürfen wir nicht vergessen, die Google-Apps-Script-API zu aktivieren, ehe wir

$ clasp login

ausführen. Das Programm meckert allerdings auch mit aussagekräftigen Fehlermeldungen, wenn man einen Schritt vergessen hat, und bietet immer Links an, um die entsprechenden Funktionalitäten zu autorisieren, OAuth2 durchzuführen oder nötige Einstellungen zu ändern.

Falls noch nicht über https://script.google.com/home geschehen, können wir jetzt auch mittels

$ clasp create my-reseller-connector

ein neues Projekt namens my-reseller-connector erstellen.

TypeScript statt JavaScript

Außerdem ist es möglich, die praktische Autovervollständigung, die der GAS-Editor im Browser bietet, nach Hause in die heimische IDE zu holen. Dazu muss man zwar TypeScript statt JavaScript schreiben, aber die Umgewöhnung ist recht leicht und der Mehrgewinn hoch.

Google Apps Script (also zum Beispiel die Code.gs-Datei, die clasp für uns angelegt hat) unterscheidet sich von JavaScript im ES3-Standard primär darin, dass es viele global verfügbare Singleton-Objekte gibt, die Zugriff auf Google-Services bieten.

Praktischerweise verwaltet clasp selbständig die Build-Phase vor dem Upload. Erkennt es ES6+, transpiliert es automatisch mittels ts2gas. Wir müssen also noch nicht mal einen TypeScript-Compiler im Projekt installieren! Einfach die Typen ins Projekt holen mit einem

$ npm install --save @types/google-apps-script

– und die IDE tut zumeist das ihrige (getestet mit Jetbrains IntelliJ IDEA und Microsofts Visual Studio Code). Hier finden sich mehr Informationen zum TypeScript-Setup.

Für dieses Beispiel verwenden wir TypeScript, weil die Typhilfen gerade am Anfang so praktisch sind.

Der Übersichtlichkeit halber benennen wir unsere Code.gs-Datei um in config.ts. Das zeigt IDE und clasp einerseits an, dass es sich hier um TypeScript handelt, sodass wir Typhilfen und die automatische Kompilierung bekommen; andererseits verdeutlicht es dem Leser, dass sich diese Datei nur um die Konfiguration des Connectors kümmert.

Die Konfiguration, die hier generiert und zurückgegeben wird, verwendet Data Studio, wenn ein Administrator den Connector als Datenquelle einrichtet. Da wir nichts einstellen können wollen, sondern bloß Anzahlen zurückgeben möchten, reicht uns Folgendes:

function getConfig(request) {
    return {};
}

Das requestObjekt, das wir hier bekommen können, wird höchstens Spracheinstellungen beinhalten, die wir respektieren könnten. Hier gibt es mehr Informationen, wie man so eine Konfiguration ausbauen könnte.

Anschließend sollten wir das Schema definieren, in dem wir unsere Daten zurückgeben wollen. Hier gibt es eine detaillierte Auflistung von Möglichkeiten, was man so alles antworten kann. Wir beschränken uns in diesem Beispiel allerdings auf unsere vier Zahl-Metriken: aktive Instanzen und entsprechende aktive User sowie Trial-Instanzen und entsprechende Trial-User. Dementsprechend könnte unsere schema.ts wie folgt aussehen:

const STATIC_SCHEMA = [
    {
        name: 'totalSubscriptions',
        dataType: 'NUMBER',
        semantics: {
            conceptType: 'METRIC',
        },
    },
    {
        name: 'totalUsers',
        dataType: 'NUMBER',
        semantics: {
            conceptType: 'METRIC',
        },
    },
    {
        name: 'trialSubscriptions',
        dataType: 'NUMBER',
        semantics: {
            conceptType: 'METRIC',
        }
    },
    {
        name: 'trialUsers',
        dataType: 'NUMBER',
        semantics: {
            conceptType: 'METRIC',
        }
    },
];

function getSchema(request) {
    return {
        schema: STATIC_SCHEMA,
    };
}

Wenn unser Schema später komplizierter werden sollte, ist es allerdings vermutlich einfacher, mit dem Data Studio Service ein Builder-Pattern zu verwenden. Dokumentation dazu findet sich hier.

So oder so erhalten wir in getSchema ein request-Objekt, das Informationen zur gewählten Konfiguration des Connectors enthalten wird.

Da wir schon wissen, dass wir keine zusätzliche Autorisierung – weder von Admin- noch von Nutzer-Seite aus – brauchen, können wir eine auth.ts definieren mit:

function getAuthType() {
    return {
        type: 'NONE',
    };
}

Zum besseren Debugging können wir unsere Logging-Statements sowohl auf den lokalen Logger als auch in Stackdrivers console.log schreiben. Dazu fügen wir eine util.ts mit einer einzelnen Funktion hinzu:

function log(message: any) {
   Logger.log(message);
   console.log(message);
}

Natürlich steht uns im Stackdriver auch structured Logging zur Verfügung, aber das ist ein Thema, das wir mal bei einer anderen Gelegenheit aufgreifen.

Connector konfigurieren

Damit können wir endlich definieren, welchen Daten wir wie zurückliefern wollen, wenn der Connector angesprochen wird. Dazu legen wir eine data.ts an. Wir beginnen damit, aus dem statischen ein dynamisches schema zu generieren, denn wenn wir zu viele Daten zurückliefern, weigert sich Data Studio, diese weiterzuverarbeiten.

///<reference path="util.ts"/>
///<reference path="schema.ts"/>

function getData(request) {
    let schema = request.fields.map(function(field) {
        for (let i = 0; i < STATIC_SCHEMA.length; i++) {
            if (STATIC_SCHEMA[i].name == field.name) {
                return STATIC_SCHEMA[i];
            }
        }
    });

Dann müssen wir Datenzeilen generieren, die allerdings jeweils nur einen Wert enthalten – nämlich den entsprechend gesuchten.

    let rows = [];
    for (let i = 0; i < schema.length; i++) {
        switch (schema[i].name) {
            case 'totalSubscriptions':
                rows.push({values: [getTotalSubscriptions()]});
                break;
            case 'totalUsers':
                rows.push({values: [getTotalUsers()]});
                break;
            case 'trialSubscriptions':
                rows.push({values: [getTrialSubscriptions()]});
                break;
            case 'trialUsers':
                rows.push({values: [getTrialUsers()]});
                break;
        }
    }

Zu guter Letzt geben wir diese generierten Zeilen auf die von Data Studio gewünschte Art zurück:

    return {
        schema: schema,
        rows: rows,
    };
}

Jetzt bleibt uns nur noch, die eigentlich Werte zu beschaffen. Um beispielsweise die aktiven Instanzen zu errechnen, müssen wir im SKU-Namen nach dem String „G Suite“ suchen, weil wir das mittlerweile eingestellte Postini nicht mitzählen wollen:

function getTotalSubscriptions(): number {
    let pageToken;
    let totalSubscriptions = 0;
    do {
        // @ts-ignore
        let result = AdminReseller.Subscriptions.list({pageToken: pageToken});
        const subscriptions = result.subscriptions.filter(function (subscription) {
            return subscription.skuName.indexOf('G Suite') >= 0
        });
        totalSubscriptions += subscriptions.length;
        pageToken = result.nextPageToken;
    } while (pageToken);
    log(`Reporting ${totalSubscriptions} total subscriptions`);
    return totalSubscriptions;
}

Aufmerksamen Lesern wird sicherlich die Compiler-Anweisung // @ts-ignore nicht entgangen sein. Die brauchen wir, weil Google derzeit noch aktiv daran arbeitet, die Typings für alle „erweiterten“ Google Services zu vervollständigen (siehe dazu auch dieses Github-Issue).

Apropos erweiterte Services: AdminReseller stellt einen solchen dar – den müssen wir also aktivieren. Dazu müssen wir die appsscript.json modifizieren. Das geht am einfachsten entweder mit clasp (clasp apis enable reseller) oder in der Weboberfläche (Resources -> Advanced Google Services). Eventuell müssen wir das auch nochmal mit OAuth2 bestätigen.

Aber wir waren ja noch nicht fertig. Die Trial-Instanzen können wir errechnen, indem wir einfach unseren Filter erweitern auf eine Abfrage nach subscription.trialSettings.isInTrial:

function getTrialSubscriptions(): number {
    let pageToken;
    let trialSubscriptions = 0;
    do {
        // @ts-ignore
        let result = AdminReseller.Subscriptions.list({pageToken: pageToken});
        const subscriptions = result.subscriptions.filter(function (subscription) {
            return subscription.skuName.indexOf('G Suite') >= 0 && subscription.trialSettings.isInTrial
        });
        trialSubscriptions += subscriptions.length;
        pageToken = result.nextPageToken;
    } while (pageToken);
    log(`Reporting ${trialSubscriptions} trial subscriptions`);
    return trialSubscriptions;
}

Bei den Benutzerzahlen müssen wir genauer aufpassen, da es je nach Lizenzmodell unterschiedlich ist, in welchem Feld die korrekte Zahl steht. Außerdem wollen wir das „free“ Tier hier ausnehmen:

function getTotalUsers(): number {
    let pageToken;
    let totalUsers = 0;
    do {
        // @ts-ignore
        let result = AdminReseller.Subscriptions.list({pageToken: pageToken});
        const subscriptions = result.subscriptions.filter(function (subscription) {
            return subscription.skuName.indexOf('G Suite') >= 0
        });
        for (let subscription of subscriptions) {
            const planName: string = subscription.plan.planName;
            if (planName.indexOf('ANNUAL') >= 0) {
                totalUsers += subscription.seats.numberOfSeats;
            }
            else if (planName == 'FLEXIBLE' || planName == 'TRIAL') {
                totalUsers += subscription.seats.licensedNumberOfSeats;
            }
        }
        pageToken = result.nextPageToken;
    } while (pageToken);
    log(`Reporting ${totalUsers} total users`);
    return totalUsers;
}

Abschließend machen wir das gleiche noch für die Trial-User:

function getTrialUsers(): number {
    let pageToken;
    let trialUsers = 0;
    do {
        // @ts-ignore
        let result = AdminReseller.Subscriptions.list({pageToken: pageToken});
        const subscriptions = result.subscriptions.filter(function (subscription) {
            return subscription.skuName.indexOf('G Suite') >= 0 && subscription.trialSettings.isInTrial
        });
        for (let subscription of subscriptions) {
            const planName: string = subscription.plan.planName;
            if (planName.indexOf('ANNUAL') >= 0) {
                trialUsers += subscription.seats.numberOfSeats;
            }
            else if (planName == 'FLEXIBLE' || planName == 'TRIAL') {
                trialUsers += subscription.seats.licensedNumberOfSeats;
            }
        }
        pageToken = result.nextPageToken;
    } while (pageToken);
    log(`Reporting ${trialUsers} trial users`);
    return trialUsers;
}

Für einen ersten Test sollte das ausreichen. Wie können wir diesen Connector jetzt in Data Studio einsetzen?

Nutzung in Data Studio

Dazu sollten wir mittels clasp push sicherstellen, dass unsere Änderungen gespeichert sind. Dann können wir mit clasp deploy ein neues Deployment erstellen (alternativ im Browser unter Publish -> Publish from manifest zu finden).

Dieses Deployment hat nun eine eindeutige ID, die wir mittels clasp deployments oder einem Klick auf Get ID finden. Diese Deployment-ID können wir im Data Studio (Link Developers) eingeben. Ein Klick auf Validate – und sofern alles geklappt hat, sollten wir jetzt unseren Connector auswählen dürfen:

Wenn wir das tun, landen wir mangels Konfiguration auf einer Seite mit einem blauen Connect-Button:

Nach einem Klick auf den Connect-Knopf sehen wir nun, welche Felder Data Studio im Schema bekannt gemacht wurden:

Wie gehabt können wir jetzt einen Report erstellen und dort unsere neu angelegte Datenquelle verwenden wie jede andere. Da wir bloß einzelne Werte anzeigen wollen, eignet sich eine Scorecard gut zur Anzeige:

Ziel erreicht! Wir können jetzt jederzeit ablesen, wie es um unsere G-Suite-Partnerschaft steht. Und da in unserer Organisation per Voreinstellung alles geteilt wird, können wir den View-Link zu diesem Report auch sofort mit allen wichtigen Stakeholdern teilen.

Ihr Partner für Google Cloud

Interessieren Sie sich für moderne Zusammenarbeit und Lösungen mit Google-Technologien? Melden Sie sich bei uns, wenn Sie Fragen rund um Google Cloud haben oder mehr wissen wollen. Wir sind offizieller Google Cloud Partner: Gerne beraten wir Sie unverbindlich zur Einführung, Lizenzierung und produktiven Nutzung von G Suite sowie zur Planung und Umsetzung von Anwendungen in der Google AppEngine oder anderen Umgebungen.

Weiterführende Infos

Google-Cloud-Frühstück: jeden Monat in Wiesbaden
Google Apps Script – Skript-Verwaltung mit dem App Scripts CLI (Clasp)
Kubernetes in a Nutshell: Einführung, Anwendungsfälle und Vorteile
Java-Anwendungen in der Google App Engine mit CloudSQL
Neue Arbeitswelten mit der Google G Suite erschließen


Mehr über die Creative-Commons-Lizenz erfahren