type holyshared = Engineer<mixed>

技術的なことなど色々

css-loaderのバージョンを上げた

webpackのcss-loaderのバージョンを5.xから6.xに変えました。
それに伴いcss内の画像指定が期待した通り変換されなくなったのでwebpackの設定ファイルを変えることで対応しました。

具体的にはwebpackの設定のrulesにasset/resourceを追加する作業です。
この設定によりcss-loaderがcssファイル内で参照している画像を置き換えてくれます。

module.exports = [
  {
    module: {
      rules: [
        {
          test: /\.scss$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader
            },
            {
              loader: "css-loader",
              options: {
                url: true,
                importLoaders: 2,
              }
            },
            "sass-loader"
          ]
        },
        {
          test: /\.(jpg|jpeg|png|svg|gif|ico)$/,
          type: "asset/resource",
        }
      ],
    },
    plugins: [
      new MiniCssExtractPlugin({}),
    ],
    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
    },
    stats: 'errors-only',
  }
];

asset/resourceをasset/inlineにすると、画像のパスをData URIに置き換えてCSSに画像データを埋め込んでくれます。
ファイルのサイズが小さい場合はこちらでも良いかもしれません。

BigQueryのテーブルに複数のログファイルをロードする

Google Cloud Storageに保存されている複数のファイルをBigQueryのテーブルに読み込もうとして、エラーが発生するので調査してみました。
エラーはテーブルにレコードが挿入できないエラーで、フォーマットが当初おかしいのかと思っていました。

Error while reading data, error message: JSON table encountered too many errors, giving up. Rows: 1; errors: 1. Please look into the errors[] collection for more details.
Error while reading data, error message: JSON processing encountered too many errors, giving up. Rows: 1; errors: 1; max bad: 0; error percent: 0
Error while reading data, error message: JSON parsing error in row starting at position 0: Could not convert value to string. Field: sign_in_count; Value: 12

しかしファイルを調べてみてもおかしいところはありません。
調べた結果、スキーマの構造が最初に処理するファイルによってスキーマが決定されていそうでした。

例えば、次のようなログファイルがあった場合、sign_int_countがNullableであることはわかりますが、INTEGERというデータ型まで特定するのは困難です。
代わりにデータ型はNullableなSTRING扱いになるようです。

{ "id": "a", "name": "demo1",  sign_in_count: null, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }

次のようだとおそらくNullableなINTEGERであることは推測できます。
なぜなら、sign_in_countに2が含まれていて、数値であることがわかるからです。

{ "id": "a", "name": "demo1",  sign_in_count: null, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }
{ "id": "b", "name": "demo2",  sign_in_count: 2, "createdAt": "2021-02-17 07:33:05 UTC", "updatedAt": null }

今までだと単一のファイルで読み込むことが多く、問題が出ませんでした。
それは単一のファイルだとフィールドのデータパターンが全部収集できるので、フィールドのデータ型が推測しやすかったからだと思います。

解決方法

この問題を解決する為に、読み込み時にスキーマを明示的に指定するようにしました。
明示的に指定することで、正しく読み込むことができます。
200ファイル近くのファイル読み込んでみましたが特に問題は起きませんでした。

その代わりautodetectのメリットがなくなります。

import { BigQuery } from '@google-cloud/bigquery'
import { Storage, File } from '@google-cloud/storage'

const storage = new Storage({ projectId: process.env.GCP_PROJECT_ID })
const bigQuery = new BigQuery({ projectId: process.env.GCP_PROJECT_ID })

const logFilesOf = async (logDate: string) => {
  const prefix = `logs/${logDate}/`
  const bucket = storage.bucket(process.env.GCP_BACKUP_BUCKET_NAME!)
  const [files] = await bucket.getFiles({ prefix })
  return files
    .filter((file: File) => file.name !== prefix)
    .filter((file: File) => {
      const size = Number(file.metadata.size)
      return size > 0
    })
}

const loadFilesOf = async (tableId: string, logDate: string) => {
  const files = await logFilesOf(logDate)

  if (files.length <= 0) {
    return
  }

  const table = bigQuery.dataset('example_dataset').table(tableId)

  return table.createLoadJob(files as any, {
    sourceFormat: 'NEWLINE_DELIMITED_JSON',
    autodetect: true,
    writeDisposition: 'WRITE_TRUNCATE',
    schema: {
      fields: [
        {
          name: 'id',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'name',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'sign_in_count',
          type: 'INTEGER',
        },
        {
          name: 'createdAt',
          type: 'STRING',
          mode: 'REQUIRED',
        },
        {
          name: 'updatedAt',
          type: 'STRING',
        },
      ],
    },
  })
}

loadFilesOf('example', '2021-08-09')
  .then(() => {
    console.log('done')
  })
  .catch((err: Error) => {
    console.log(err.stack)
  })

Cloud CDNでの署名付きCookie、署名付きURL

Cloud Storageに格納したファイルをCDNで配信する時にアクセスを限定したい場合があります
その際に署名付きCookie、署名付きURLが使えます

設定自体はterraformなどで、自動で設定して置けるようにすると楽です

resource "google_storage_bucket" "public-images" {
  project  = var.project_id
  name     = "public-images-${var.project_id}"
  location = "asia-northeast1"

  force_destroy = true

  cors {
    origin          = ["https://${var.domain}"]
    method          = ["GET"]
    response_header = ["*"]
  }
}

resource "google_storage_bucket_iam_member" "signed-url-user" {
  bucket = google_storage_bucket.public-images.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:service-${var.project_number}@cloud-cdn-fill.iam.gserviceaccount.com"

  depends_on = [
    google_compute_backend_bucket_signed_url_key.backend_key
  ]
}

resource "google_compute_backend_bucket" "cdn-backend-bucket" {
  project     = var.project_id
  name        = "backend-${google_storage_bucket.public-images.name}"
  bucket_name = "public-images-${var.project_id}"
  enable_cdn  = true
}

resource "random_id" "url_signature" {
  byte_length = 16
}

resource "google_compute_backend_bucket_signed_url_key" "backend_key" {
  project        = var.project_id
  name           = "public-backend-bucket-key"
  key_value      = random_id.url_signature.b64_url
  backend_bucket = google_compute_backend_bucket.cdn-backend-bucket.name
}

resource "google_compute_url_map" "cdn" {
  project         = var.project_id
  name            = "public-images-cdn"
  default_service = google_compute_backend_bucket.cdn-backend-bucket.id
}

resource "google_compute_target_https_proxy" "cdn-proxy" {
  project          = var.project_id
  name             = "proxy-${var.project_id}"
  url_map          = google_compute_url_map.cdn.id
  ssl_certificates = [google_compute_managed_ssl_certificate.tls.id]
}

resource "google_compute_managed_ssl_certificate" "tls" {
  project  = var.project_id
  provider = google-beta
  name     = "public-images-${var.project_id}"
  managed {
    domains = [var.domain]
  }
}

resource "google_compute_global_forwarding_rule" "cdn" {
  project    = var.project_id
  name       = "public-images-${var.project_id}"
  target     = google_compute_target_https_proxy.cdn-proxy.self_link
  port_range = 443
}

生成された鍵はSecretManagerなどに登録しておくといいと思います

署名付きCookie

cloud.google.com

署名付きCookieは署名付きリクエストの鍵を使ってCookieに署名します
生成した値はCloud-CDN-Cookieに指定して利用できます

import crypto from 'crypto';
import dayjs from 'dayjs';

export const ENCODED_URL_PREFIX = (Buffer.from(process.env.URL_PREFIX))
  .toString("base64")
  .replace(/\+/g, "-")
  .replace(/\//g, "_");

export const signedCookie = (seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();
  const input = `URLPrefix=${process.env.ENCODED_URL_PREFIX}:Expires=${unixTimestamp}:KeyName=${process.env.SIGNED_URL_KEY_NAME}`;
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(input)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  return `${input}:Signature=${signature}`;
}

署名付きCookieの場合はCookieのパスの指定ミスなどで期待通りブラウザがCookie送ってくれなかったり、原因の切り分けを早くできた方がいいので、 実装が済んでしまったら先に正しい署名がCookieについているか確認しておくと良いと思います

curl --cookie='Cloud-CDN-Cookie=[生成した値]' \
  -D dump-headers.txt \
  --output=example.jpg \
  https://cdn.example.com/example.jpg

署名付きURL

cloud.google.com

署名付きURLはURLに対して署名を付加できるものです
Cookieと違って対象のURLが多い場合面倒ですが、実装自体は簡単なので導入しやすいです

import crypto from 'crypto';
import dayjs from 'dayjs';

export const signedURL = (url: string, seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();

  const segments = [
    `?Expires=${unixTimestamp}`,
    `&KeyName=${process.env.SIGNED_URL_KEY_NAME}`
  ];

  const signURL = `${url}${segments.join('')}`
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(signURL)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  segments.push(`&Signature=${signature}`);

  const singedQuery = segments.join('');

  return `${url}${singedQuery}`;
}

こちらも実装が済んだら検証しておきます

curl --output=example.jpg [署名付きURL]

SOLOVAIRの新しいカラーのブーツ買った

新しく青系のカラーが3色追加されたみたいなので、一足買ってみました。
SOLOVAIRのアストロノーツブーツを一足持っていて、サイズ感はわかっていたので、特に困ることはなかったです。

f:id:holyshared:20210618111638j:plain:w500
SOLOVAIR Neon Blue 8Eye Boot

また、自分は履く靴によってソックスを変えているのですが、アルパカの毛が使用されているソックスが気になったのでそれも一緒に購入しました。

f:id:holyshared:20210618111735j:plain:w500
SOLOVAIR Socks

アルパカのソックスを履いて歩いてみましたが、チクチクしないニットぽい感触で特に不快感もなく過ごせました。 ブーツ履く用に使用して行きたいと思います。

SentryのSDK問題

SentryのjavascriptSDKを6.5.1にアップグレードしたら初期化に失敗するようになった。

options._metadata.sdkがないのでエラーになるようだ。
ただ内部的に付与されるぽいプロパティな感じがするので、何か必須のパラメータがあるのかなと思ったけどそれらしいものはなさそうでした。

一応強制的に指定するようにしたら出なくなり、一応動くようになった。
しかし気持ち悪い。

import * as Sentry from "@sentry/node";
import * as Tracing from "@sentry/tracing";

Sentry.init({
  dsn: 'real dsn',
  _metadata: { sdk: null }, // これを追加した
  integrations: [new Tracing.Integrations.Express()],
});

追記

手元で最小構成で試してみたら再現できなかったので、SentryのSDKの問題ではないことがわかった。
アプリケーションはwebpackでbuildしていたので、buildし直してみたら発生しなくなった。

解決した理由は下記のどれかと思われる。 * typescript周りのパッケージをアップグレード * babel周りのパッケージをアップグレード

一旦応急処置はしたのでパッケージのアップグレードでもするかーとやっていたら直った感じ

Oura Ringのバッテリーの問題

Oura Ringを半年くらい使用していたのだけど、突然充電できなくなりました。 大体10分くらいで充電が終わるのだけど全然できなくて困ってました。

多分故障だろうと思って新しく買わないとダメなのかなと思っていたら、購入から2年以内は交換できるぽかったので、アプリから問い合わせてみました。

サポートから1日ぐらいで連絡が来て、バッテリーの問題ぽいので、注文のIDとか住所を聞かれ、回答したら新しいやつを送ってくれることになりました。

サポートの対応が早くてめちゃくちゃ助かりました。

今日届いて、新しいリングも無事ペアリングでき半日つけてみた感じ、消費カロリーとか記録もトラッキングできてるぽいです。

同じように困っている人がいたら問い合わせてみた方が良いかもしれません、

画像を投稿した後にレポートを出す

投稿後の結果
レポート

自作アプリで写真を投稿後に今の集計値を確認したくてページ遷移することがあり不便だったので投稿後に集計を表示するように変えた

デプロイしてから、数日使って見ている感じストレスが減ったので実装してよかったです。   この前進捗状況を表示するように変えたけど、それも表示したい。