Flutter Get data from Google Search Console

eye-catchDart and Flutter
Sponsored links

To keep running a blog, it’s necessary to have a tool like Google Search Console for the analysis because I want to collect many visitors to my website. To collect visitors, an article needs to appear in a high-rank position on the search result.

I use Google Search Console to know which keywords visitors give to come to my page and which keywords are in a good position for Google search. However, I need to switch the page whenever I want to know different keywords (queries) results or want to see the result of a different page.

Since I want to see the result quicker, I’ve tried to use Google Search Console API. Very few websites write about this topic and I didn’t find any good resources when I tried, I share what I learned.

The code written in this post is not included in my Flutter sample repository.

Sponsored links

Google Cloud Platform

We need to create a project in Google Cloud Platform.

Enable Google Search Console API

After creating a project, the Google Search Console API needs to be enabled.

Go to Library

Search “Google Search Console”, then you can find this. The API is already enabled in the image.

Check target scope

The scopes screen appears while creating a project but you might already create it and get lost. In this case, you can go to the setting page from “OAuth consent screen > EDIT APP > SAVE AND CONTINUE”.

Google Search Console API needs to be checked in the scopes screen. If the entry doesn’t exist in the list, you need to enable the API first explained above.

Sponsored links

Get Access Token by Google sign in

To get data from Google Search Console, we need to have an access token. Add the following dependencies to “pubspec.yaml“.

dependencies:
  googleapis: ^7.0.0
  google_sign_in: ^5.2.1

The login code looks like this.

Map<String, String> authHeader = {};

Future<GoogleSignInAccount?> signIn() async {
  final googleUser = await googleSignIn.signIn();

  if (googleUser == null) {
    return null;
  }

  afterLoginProcess(googleUser);
  return googleUser;
}

Future<void> afterLoginProcess(GoogleSignInAccount googleUser) async {
  final googleAuth = await googleUser.authentication;
  // Access-Token
  authHeader = {"Authorization": "Bearer ${googleAuth.accessToken}"};
}

I’ve also written the following posts. The login code looks a bit different but it might be helpful. Firebase is not mandatory.

For mobile

For web

Set an Access Token to Google Auth Client

We got an access token for Google API. We need to set it to an HTTP client that is used in the API instance. This is a simple version without retry.

class GoogleAuthClient extends http.BaseClient {
  final _client = http.Client();

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    request.headers.addAll(authHeader);
    return _client.send(request);
  }
}

I tried to write the following code for retry. It resends the data if the current access token is expired but I’ve not tested it well so far because it takes 30 minutes until the access token is expired.

“Can’t finalize a finalized Request” error might occur. If it’s possible to forcibly get the current access token expired, I want to test the code. If you know how to do it, please let me know.

class GoogleAuthClient extends http.BaseClient {
  final _client = http.Client();

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    var originalHeaders = {...request.headers};

    try {
      request.headers.addAll(authHeader);
      var response = await _client.send(request);
      if (response.statusCode == HttpStatus.unauthorized) {
        response = await _resend(originalHeaders, request);
      }
      return response;
    } catch (e) {
      print(e);
      rethrow;
    }
  }

  Future<http.StreamedResponse> _resend(
    Map<String, String> originalHeaders,
    http.BaseRequest request,
  ) async {
    final googleUser = await googleSignIn.signInSilently();
    if (googleUser == null) {
      throw const UnauthorizedException();
    }
    await afterLoginProcess(googleUser);
    originalHeaders.addAll(authHeader);
    request.headers.clear();
    request.headers.addAll(originalHeaders);
    return _client.send(request);
  }
}

How to get data via SearchConsoleApi

This is the main point. We need to create an instance of SearchConsoleApi first. The target website needs to be specified in the query later, so we pass it here.

import 'package:googleapis/searchconsole/v1.dart' as sc;

class DataReader {
  final sc.SearchConsoleApi api;
  final String site;
  DataReader(this.api, this.site);
  ...
}

// instantiate
final api = sc.SearchConsoleApi(GoogleAuthClient());
final dataReader = DataReader(api, "https://www.technicalfeeder.com/");

Query parameters for grouping

The next is a query to get desired data. dataState can be “ALL” or “FINAL”. The difference is whether to include partial data or not.
dimentions” is the same as “group by” for SQL. I thought it will be columns to be shown but not. In this case below, the data is grouped by the combination of “query“, “page“, and “date“. The query result, e.g. clicks, depends on this setting. If “date” is not given here, the number of clicks will be sum of the period.

If we want to know the total number of clicks in a certain period, which means visitors click the page on the search result, we need to specify only “page” there.

import 'package:googleapis/searchconsole/v1.dart' as sc;

class DataReader {
  final sc.SearchConsoleApi api;
  final String site;
  DataReader(this.api, this.site);

  Future<List<RowData>> fetch({
    String? startDate,
    String? endDate,
  }) async {
    final result = await api.searchanalytics.query(
      sc.SearchAnalyticsQueryRequest(
        dataState: "ALL",
        startDate: startDate,
        endDate: endDate,
        rowLimit: 50,
        dimensions: [
          "query",
          "page",
          // "country",
          // "device",
          "date",
        ],
      ),
      site,
    );
    ...
  }
}

Possible data kind into a class

After getting the data, we probably need to work with them. If we have a class for the data, it is easier to handle them. The properties listed in this class are all that we can get from SearchConsoleApi.

class RowData {
  int clicks;
  double ctr;
  double impressions;
  double position;
  String query;
  String page;
  String? country;
  String? device;
  String date;

  RowData({
    required this.clicks,
    required this.ctr,
    required this.impressions,
    required this.position,
    required this.query,
    required this.page,
    this.country,
    this.device,
    required this.date,
  });
}

Since we prepared the class, let’s add data into it.

class DataReader {
  final sc.SearchConsoleApi api;
  final String site;
  DataReader(this.api, this.site);

  Future<List<RowData>> fetch({
    String? startDate,
    String? endDate,
  }) async {
    final result = await api.searchanalytics.query(
      sc.SearchAnalyticsQueryRequest(
        dataState: "ALL",
        startDate: startDate,
        endDate: endDate,
        rowLimit: 50,
        dimensions: [
          "query",
          "page",
          // "country",
          // "device",
          "date",
        ],
      ),
      site,
    );

    // Data mapping
    return result.rows?.map((e) {
          final json = e.toJson();
          final keys = _readOf(json, "keys");
          return RowData(
            clicks: _readOf(json, "clicks"),
            ctr: _readOf(json, "ctr"),
            impressions: _readOf(json, "impressions"),
            position: _readOf(json, "position"),
            query: keys[0],
            page: keys[1],
            date: keys[2],
          );
        }).toList() ??
        [];
  }

  dynamic _readOf(Map<String, dynamic> row, String key) {
    final data = row.entries.firstWhere(
      (element) => element.key == key,
    );
    return data.value;
  }
}

The value of keys[X] depends on the order of the “dimensions” value. keys[0] contains query because it is specified at the first element. If the first element is “page“, key[0] contains a page URL.

The result looks like this below.

End

I could get the data from Google Search Console, so the next step is to show the data on a graph. I want to show a graph when I click one of queries but the query list keeps the entries while Google Search Console shows only the selected query. If the list keeps the entries, the graph can switch smoothly and quickly without interruption.

Comments

Copied title and URL