dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Vercel OG(@vercel/og)でOG画像を動的生成する

こちらの記事を読んで。

以前から https://reading-list.zaki-yama.dev/ という自分のサイトに OG 画像を設定したいな〜と思っていたところに @vercel/og なるライブラリが登場したので試してみた。

※ なお、今までずっと「OGP 画像」だと思ってたんだけど、記事中では OG image と表記されていたので、ここでも「OG 画像」と表記する。

@vercel/og の特徴

冒頭の記事を読む限り、今までは vercel/og-image というサービスが提供されていたが、これには以下のような問題があった。

  • 難しい: Serverless Function 内で Chromium を起動し、 Puppeteer でスクリーンショットを撮る、ということをやっていた。これらのツールのセットアップは実装が難しくしばしばエラーを引き起こしていた
  • 遅い:Chromium を Serverless Function 内で起動するため圧縮し、コールドブート中に解凍される必要があるため、遅い(平均4秒)。結果、ソーシャルカードの生成が遅くなったり壊れたりした

今回発表された @vercel/og はこれらの問題を解決したもので、具体的には以下の特徴がある。

  • 簡単 (Easy):ヘッドレスブラウザ不要。HTML と CSS から OG 画像を生成できる
  • 高速 (Fast): vercel.com/docs で試した結果 vercel/og-image より 5倍程度速かったとのこと

加えて @vercel/og を利用したコードは Next.js アプリケーションと共存でき、 vercel/og-image のように別のところにデプロイする必要がない というのもうれしい。

@vercel/og が使える条件

https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#requirements を読む限り

  • Next.js のバージョンが v12.2.3 以降
  • runtime: 'experimental-edge' を有効にして Edge Runtime を使用する

を満たす必要がある。後者はデプロイ先が Vercel であれば問題にならないが、他だとどうなんだろ。やったことないのでわからない。

試してみる

というわけで自分で運用している Next.js アプリケーションの https://reading-list.zaki-yama.dev に @vercel/og を導入してみる。

ソースコードhttps://github.com/zaki-yama/reading-list

公式ドキュメントとしては以下を読めばよさそう。

簡単な OG 画像を表示する

まずは、ライブラリを導入して静的な OG 画像を表示する。
インストールは以下のコマンドで行う。

$ npm i @vercel/og

続いて、OG 画像生成用の API エンドポイントを用意する。
/pages/api/og.tsx というファイルを作る。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';

export const config = {
  runtime: 'experimental-edge',
};

export default function () {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    },
  );
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#getting-started を引用)

これで npm run dev して http://localhost:3000/api/og にアクセスすると Hello world! という画像が表示される。

最後に、これが OG 画像として表示されるようにする。それには <head> に以下のように meta タグを埋め込めばよい。
(自分は Layout コンポーネント内に定義した)

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og"
/>

テキストを動的に設定する

基本的な使い方がわかったので、次は画像内のテキストを動的に設定してみる。
具体的には、現在開いているページのタイトルを OG 画像に埋め込むようにしたい。

OG Image Examples > Dynamic text generated as image にちゃんとサンプルが用意してある。

まず、 /pages/api/og.tsx 側は以下のように、クエリパラメータでタイトルを受け取るようにする。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';

export const config = {
  runtime: 'experimental-edge',
};

export default function handler(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);

    // ?title=<title>
    const hasTitle = searchParams.has('title');
    const title = hasTitle
      ? searchParams.get('title')?.slice(0, 100)
      : 'My default title';

    return new ImageResponse(
      (
        <div>
            // ...略...
            {title}
        </div>
      ),
      {
        width: 1200,
        height: 630,
      },
    );
  } catch (e: any) {
    console.log(`${e.message}`);
    return new Response(`Failed to generate the image`, {
      status: 500,
    });
  }
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-examples#dynamic-text-generated-as-image から抜粋)

そして、使う側は

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og?title=${postData.title}"
/>

というようにクエリパラメータをページごとに変えて設定してやる。

1点サンプルには載ってなかったこととして、自分は以下のように URLSearchParams をかませるようにした。

// /pages/posts/[id].tsx
export default function Post({
  postData,
}: {
  postData: {
    title: string;
    date: string;
    contentHtml: string;
  };
}) {
  const title = `${siteTitle} ${postData.title}`;
  const searchParams = new URLSearchParams(`title=${title}`);
  return (
    <Layout>
      <Head>
        <title>{title}</title>
        <meta
          property="og:image"
          content={`https://reading-list.zaki-yama.dev/api/og?${searchParams.toString()}`}
          key="og-image"
        />
        <meta property="og:title" content={postData.title} key="og-title" />
      </Head>
      ...

https://github.com/zaki-yama/reading-list/blob/main/pages/posts/%5Bid%5D.tsx#L17-L27

おそらくサイトや記事のタイトルに ' (スペース)が含まれていたせいで Open Graph Debugger で見たときにうまく表示されなかったため、これらの文字は URL エンコードするようにした。

結果

https://reading-list.zaki-yama.dev/posts/2022-09-13 という URL を Twitter や Slack に貼ったときに OG 画像が表示されるようになった。

デザイン性皆無なのはしょうがないが、一旦満足。

2022年7〜9月のふりかえり

前回(4〜6月)のふりかえりを完全に忘れてしまっていた。。。

前回:2022年1〜3月のふりかえり - dackdive's blog

もはや前回何を書いてたのか思い出せないが、このままずるずると書かなくなってしまいそうだったので頑張って書く。

✨ やったこと

CRE (Customer Reliability Engineer) としての活動

年明けぐらいから少しずつ社内で CRE というロールを担当しており、その活動内容をブログに書いた。

業務内容をきちんとした記事で公開するのは初めて。


UoPeople: CS2203 Databases 1

2021-22 T5(2022年6月〜8月)はデータベースの基礎的な講義を受講した。
基礎といっても正規化だなんだと今まできちんとデータベースを学んでこなかった自分には厳しかった。。。そしてこの感想も記事にしてないや。


伸ばしたいことリストを作った

職場の同僚に紹介してもらっていいなーと思ったのだが、自分が今伸ばしたいと思っているスキル(技術に限らない)をリストにして、月1ぐらいで棚卸しするようにした。

今はこんな感じ。
自分は興味関心の移り変わりが激しいので、せめてその月だけは上位のテーマをちゃんと探求する制約になればいいなと思う。
あと、今後はリストの棚卸しと合わせて振り返りもするのがいいんじゃないかと思った。

📝ブログ・登壇

イベント登壇は0件。ブログ執筆は以下。

💬 所感

プライベート

この四半期はなんといっても第三子が産まれたというのが自分の人生における大きなイベントだった。

3人目ともなればいい加減子育ても慣れたもんかなと思ったら全然そうでもなくて日々死にそうになりながら育児と仕事をやっている。
久々の新生児はやっぱりかわいい。

あと、歳をとったせいか今まで以上に体力の衰えを感じる。
具体的には夜22時ぐらいになるとむちゃくちゃ眠い。せっかく時間ができてもプライベートの勉強をやる気力が出ず、なかなかやりたいと思ったことがやれない状況が続いている気がする。
運動を習慣化する、というのが今一番必要な取り組みなのかもしれない。

仕事

仕事面では、現職の業務内容がこれまでにやったことがない領域にチャレンジできていて非常に充実している。
反面、フロントエンドもまだまだだなーと思うことが多くもう少し勉強が必要だと感じる。

学業

仕事の方で学ばないといけないことがたくさんある & 学びたいというモチベーションもあるせいか、UoPeople へのモチベーションが若干下がっている感ある。
8月からの term も結局第一週でリタイヤしてしまったし。。。

11 月からはさすがに受講したい。

💪次の四半期(2022年10〜12月)のテーマ

とりあえず10月は react-testing-library にもう少し詳しくなろうかな。
11月から大学の方も新学期始まるはずなのでまた忙しくなりそう。

💸 買ったもの

めちゃくちゃおもしろかった。

ECS とか学びたくて買ったがまだ積読。。。

React公式ドキュメントの「You Might Not Need an Effect」を読んだ

少し前に話題になっていた以下のドキュメントをようやく読んだ。

beta.reactjs.org

記事を書いた時点でドキュメントはまだベータという位置づけ。

長いので先に目次を。
⭐ は中でも特にへえ〜って思ったやつ。💬 はコメント。

続きを読む

スプレッドシートのピボットテーブルで「総計を表示」しているかどうかをGASで判定する

超小ネタ。

Google スプレッドシートでピボットテーブルを作ったとき、行または列の項目ごとに「総計を表示」するかどうか選択できる。
この値を GAS で取得したい。

なんでこんなこと思ったかというと、総計を表示するかどうかで行・列数が変わるため、処理に分岐が必要だったから。

方法

PivotGroup クラスに totalsAreShown() メソッドがある のでそれを使う。

function getTotalsAreShown() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('foo');

  const pivotTables = sheet.getPivotTables();
  const pivotTable = pivotTables[0];

  // 列の場合。行の場合は getRowGroups()
  const pivotGroups = pivotTable.getColumnGroups();

  pivotGroups.forEach(pivotGroup => {
    console.log(pivotGroup.totalsAreShown());
  })
}

SheetPivotTablePivotGroup という3つのクラスが関係しており、それぞれの関係性は下の図のとおり。

なお、 showTotals(boolean) というメソッドで ON/OFF の設定もできる。

参考リンク

Backyard Hatena「#12 id:missasan に聞くMackerelのカスタマーサクセス」を聴いたメモ

貴重な CRE 事例のお話ということで聴いた。以下メモ。

  • [1:50] CREとしての missasan はどんなことやってるの?
    • MackerelのCREは大きく2軸
      • テクニカルサポート
      • カスタマーサクセス←← missasan さんはこっち
  • [2:30] カスタマーサクセスって言葉も耳慣れないかもだけど、どんなことをやる?
    • サブスクリプションモデルのSaaSだと浸透してきたよね
    • 契約した「後」のお客様に対し、プロダクトを利用することによって得られる成果(=成功)を期待通り、期待以上にする
    • 非エンジニアがやってる会社も多い
  • [4:41] CR”E” なのでエンジニアでもあるんですよね?
    • Mackerelはサーバー監視ツールなのでユーザーはエンジニア
    • プロダクトの特性的に自然とエンジニアの知識が求められる→自然とMackerelのCREはエンジニアに
  • [6:00] Mackerelが提供する「サクセス」
    • 難しい。まさに今定義していってるところ
    • サーバー監視でビジネス上どういう成功につながるか。サーバー監視ってプラスというよりかは守り、保険みたいなイメージなので
    • SREとかDevOpsの知識を取り入れてプラスの面を出していきたい
    • Mackerel使うとこういうふうにステップアップできますよ、という良い世界感を作っていく、ということを考えてる
  • [10:10] 世界観を作っていくためにやっていること
    • いろんなロールの人と話している。SREの方とかと定期的に1on1
    • Mackerelのカスタマーがどう成功していくのかちゃんと考えたい→ジャーニーとか成熟度とか、どういう監視の考え方があるのか
    • 一方で、CREがカスタマーサクセスとして日々お客様と接していて、そこで得たユーザー像を開発チームにもっとインプットしていこうと考えて、インプット先として選ばれたのがデザイナーチーム
      • こういうユーザーがいて、こういうペルソナで、こういうカスタマージャーニーで、を伝える、または一緒に作る会
      • そこで作ったカスタマージャーニーをまたSREにフィードバック
    • motemen: いろんな人と話してるんですね
      • ですね。ほぼ全職種
      • こないだまでは登壇もあったのでマーケと連携したりとか、プリセールスみたいな仕事もやってるのでセールスの人とも話したりとか
  • [15:00] お客様のフロントに立ってお客様の声を聞く立場だから、その声をデザイナーや開発チームにフィードバックしている、と
    • ですね
    • プロダクトチームが「お客様にこうなってほしい」って考えて出した機能、意外とお客様に届かない。そこを「こういう機能ありますよ」って気づいてもらうとか「お客様の場合はこう使うと便利ですよ」みたいなアシストをする
    • 逆にお客様はお客様で「こうなりたい」というものを持っている。両者を近づけるのがCREの役目
  • [17:00] お客様の要望・困りごとを開発チームに届けるのはどうやってやっている?
    • いろんな変遷があったが、最近は開発チームのバックログとは分けて管理。セールスやカスタマーサクセス担当から挙がってくるものをなんでもかんでもストックしていく
    • それだけだと開発チーム全部読んで判断するの難しいので、トピックやセグメントで絞ったものをレポートしてる
      • 月1で共有会。開発チーム全員任意参加
    • トピックで絞る、とは?
      • わかりやすいのでいうと機能
      • あとはお客様がMackerelを使うシーンでの分類。初期設定の話なのか障害対応中なのか障害後のふりかえりなのか
  • [21:20] 要望を開発チームにフィードバックすることで良い変化はあった?
    • 一番うれしい変化は実際に開発されること
      • 当時は「難しいよね」で眠ってたものが掘り起こされてリリースに至った、とか
    • 関連する機能を作るときに「そういえばこういう要望あったな」というのもアイディアの種になると思っている
    • 「ユーザーのことで知りたくなったらCREに聞けば何か返ってくる」という認知ができた
      • CRE、いろいろやってて社内でも何してるかわからなかった
  • [25:15] missasan がエンジニアとして手を動かすことってある?
    • あります
    • お客様向けにMackerelの機能紹介をするとき、デモ用の環境作るのでそのときは手を動かす
    • エンジニアの要素も必要
      • 今後、ビジネス要素の強い部分とエンジニア要素の強い部分とで業務を分けて、それぞれで専門性高い人に入ってもらえるようにするというのも検討している
      • Mackerelユーザーのインフラ事情も多種多様。オンプレ、パブリッククラウド、etc.
  • [29:30] missasan: はてなのSREと一緒に仕事できる、現場の運用やってるエンジニアと会話しながら仕事できるというのがMackerel CREの面白いところかな
  • [30:30] 逆に missasan から僕に聞きたいことありますか?
      1. CREがこうなってくれたらいいな、みたいな期待値あります?
      2. 今 missasan がカスタマーサクセスに興味持たれて、そこに注力してるのは一歩前進してるなーと感じてる
      3. Mackerel使うとこんなふうにビジネス・サービス成長しますよ、を提示できるようになってくと良い
      4. SRE的な知識に裏打ちされたプロダクトビジョンに、お客様の声をうまく反映できるといい

感想

弊社でいうとカスタマーサクセスチームの責務・立ち回りにかなり近いなと感じた。お客様からの声をプロダクトチームにフィードバックするような動きとか。

お客様と日々向き合うことで得られたユーザー像をデザイナーチームと連携してペルソナやカスタマージャーニーを作っていく、という活動はめちゃくちゃいいな〜(小並感

StorybookのInteraction Testをようやく試した

社内の10分勉強会駆動でようやく Storybook の Interaction Test についてまとめることができたので、メモ。

Interaction Test とはなにか

Storybook 上でユーザーのインタラクション(ボタンクリックやフォーム入力など)を再現できる機能。
上の GIF のように Storybook でインタラクション結果を確認できるだけでなく、インタラクションを記述したスクリプト中にアサーションを書いてテストとして機能させることもできる。また書いたスクリプトTest Runner により CLI や CI で実行することも可能。

使い方

公式ドキュメントの

に従う。

また公式ブログの

チュートリアル形式で実際に試せるのでわかりやすい。
(上記のチュートリアルを写経したものを https://github.com/zaki-yama-labs/storybook-interaction-test-example に置いてある)

$ yarn add --dev @storybook/testing-library @storybook/jest @storybook/addon-interactions

で必要なパッケージをインストールした後、Story に対し play function と呼ばれる関数を記述する。

冒頭の GIF に相当する play function は以下。

import React from "react";
import { within, findByRole, userEvent } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import { InboxScreen } from "./InboxScreen";

export default {
  component: InboxScreen,
  title: "InboxScreen",
};

// ...中略...

export const EditTask = Template.bind({});
EditTask.parameters = { ...Default.parameters };
EditTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole("listitem", { name });

  const itemToEdit = await getTask("Fix bug in input error state");
  const taskInput = await findByRole(itemToEdit, "textbox");

  await userEvent.type(taskInput, " and disabled state", { delay: 300 });
  await expect(taskInput.value).toBe(
    "Fix bug in input error state and disabled state"
  );
};

https://github.com/zaki-yama-labs/storybook-interaction-test-example/blob/e116e542b1ba652afd4345c1ee8d6f279b39e965/src/InboxScreen.stories.js#L69-L82

react-testing-library でのテストを書いたことがある人からすると、基本的な書き方は大きく変わらないのがわかる。ただ import しているライブラリは @storybook/xxx になっている。

この play function を書いた状態で該当の Story を開くと、下部のペインに Interactions というタブが追加される。インタラクションは Story を開いたときに自動的に開始されるが、 Interactions タブ内の ◀️ ▶️ をクリックするとコマ送りで確認することもできる。

CLI から実行する

@storybook/test-runner をインストールすると test-storybook というコマンドが使えるようになるので、これを CLI から実行する。このとき、

  • play function が定義されていない Story に対しては:Story がエラーなくレンダリングされるかどうかを検証する
  • play function が定義されている Story に対しては:play function がエラーなく実行され、すべてのアサーションが pass するかどうかを検証する

という挙動になる。

Interaction Test で何がうれしいの?

ユースケースを模したテストを目視で確認しながら書けるのでわかりやすいですね、ということに加え、 VRT(Visual Regression Testing)と組み合わせるとよりおいしい んだろうなと思った。

VRT(Visual Regression Testing)

  • UIを画像として保存しておいて差分検出するスナップショットテストの一種
  • マネジドサービスになったものとしては Chromatic が有名(というか他を知らない)
  • OSS でも reg-suit & storycap というツール + S3 などのオブジェクトストレージ組み合わせると比較的簡単に構築できる

Interaction Test + VRT = ?

  • Storybook だけだと難しい「ユーザー入力に伴うUIの状態」を担保してくれる
  • アサーション書いてないところも含めて意図せずUI崩れてないか担保してくれる

ちなみに、VRT って Interaction Test の操作ちゃんと待ってくれるの?flaky にならない?というのが気になったが、こちらの記事で reg-suite と storycap の作者が特に問題なかったと言及していた。

Storybook の play function と VRT - Qiita

わからないこと

というわけで非常に便利そうではあるが、元々 react-testing-library + Jest で書いてたテストとの棲み分けがどうなるのかはちょっとまだ整理ができていない。
play function 内にアサーションも書けるので、極論すべてのコンポーネントのテストを play function に寄せることもできるのでは?と思ったけどどうなんだろう。そうなると実行速度だけが判断基準か。

takepepe さんのこちらの記事では、逆に play function を定義した Story を Jest に取り込む、とある。この場合あくまでテストは Jest 側に集約するイメージだと理解した。

参考リンク

自作Chrome拡張をManifest V3に移行したメモ

という Chrome 拡張を作っているのだが、先日ようやく Manifest V3 に移行したのでそのときにやったことのメモ。

該当の PR はこれ。

また、作業中は Zenn のスクラップにメモを取りながら進めていた。
自作Chrome拡張をManifest v3に対応させる

モチベーション

Manifest V3 移行へのモチベーションはそこまで高くなかったが、vite を触ってみるため webpack からの移行を試してみたいなと思っていた。
そんな折

Chrome拡張 つくりかた 令和最新版

という記事を拝見して、自分もこのとおりにやって乗り換えようと思った。が、どうやら @crxjs/vite-plugin というプラグインは Manifest V3 にしか対応してないということがわかり、先に V3 移行を済ませるようにした、という経緯。

参考:いつまでに Manifest V3 に移行すべきか

The transition of Chrome extensions to Manifest V3 - Chrome Developers

によれば、 2022年1月からすでに新規の Chrome 拡張は Web ストアで公開することができず、既存のものについても 2023年1月以降は動作しなくなるらしい。

どう進めたか

最初に

Manifest V3 migration checklist - Chrome Developers

を見て自分があてはまる項目を確認し、あとはそこから参照されているリンクも読みつつ適宜移行を進めていった。

自分の Chrome 拡張はページのタイトルと URL をコピーするだけのシンプルな機能で、そんなに複雑な API を使っていない。チェックリストであてはまったのは以下:

  • Are you using background pages?
  • Are you using the browser_action or page_action property in manifest.json?
  • Are you using the chrome.browserAction or chrome.pageAction JavaScript API?

やったこと1: browserAction を action に移行

V3 では browser action と page action という区別がなくなり、action で統一されるらしい。
そのため manifest.json 内の xxx_action という記述およびソースコード内の chrome.xxxAction を action に変換する。

コミット:https://github.com/zaki-yama/copy-title-and-url-as-markdown/pull/221/commits/b259acbba3520727e2bc49a5260eff86740580c7?diff=unified&w=0

manifest.json

   "commands": {
-    "_execute_browser_action": {
+    "_execute_action": {
       "suggested_key": {
         "default": "Ctrl+Shift+C",
         "mac": "MacCtrl+Shift+C"
@@ -29,7 +29,7 @@
       "description": "Copy as optional format #2"
     }
   },
-  "browser_action": {
+  "action": {
     "default_icon": "icon.png",
     "default_popup": "popup.html"
   },

background.ts

-      chrome.browserAction.setBadgeText({ text: formatIndex });
+      chrome.action.setBadgeText({ text: formatIndex });

1つポイントとしては、manifest の commands でポップアップのショートカットキーを設定している場合、ここも _execute_action に変える必要がある。

やったこと2: background page を Service Worker 化する

コミット:https://github.com/zaki-yama/copy-title-and-url-as-markdown/pull/221/commits/c57581e1dfa03bafe320ca190698b4f112e28280

V3 からは background page という概念はなくなり、 Service Worker になる。

ここで少々困ったのが、元々ポップアップとは別のショートカットキーでもページのタイトルとURLをクリックできるよう、 background page を使用していた。(コピー時のフォーマットとして、ポップアップ起動時とは別のフォーマットをオプションで設定できる)

そのしくみとして、background page の HTML 内にダミーの textarea 要素を置き、そこに値をセットして select してから document.execCommand("copy") を呼ぶという方法を取っていた。しかし、V3 では HTML がなくなったためこの方法は使えない。

createElement すればいいかと思っていたが、冒頭の

Chrome拡張 つくりかた 令和最新版

を書いた方が公開している拡張のソースコードを見て、なるほどそんなやり方もあるのかと知ったためそ方法を真似させていただいた。

https://github.com/r7kamura/copy-rich-link/blob/main/src/main.ts#L3-L12

また別の問題として、Service Worker から直接は document などのオブジェクトは触れない。そのため、 Scripting API というものを使う。

// copyToClipboard の実装は↑で紹介したものとほぼ同じ
import { escapeBrackets, copyToClipboard } from "./util";

chrome.commands.onCommand.addListener((command) => {
  const queryInfo = {
    active: true,
    currentWindow: true,
  };

  chrome.tabs.query(queryInfo, function (tabs) {
      // (中略)

      chrome.scripting.executeScript({
        target: { tabId },
        func: copyToClipboard,
        args: [options[key], title, escapeBrackets(url)],
      });
   ...

chrome.scripting.executeScriptAPI ドキュメントはここ:
https://developer.chrome.com/docs/extensions/reference/scripting/#method-executeScript

func に呼び出したい関数を渡すことができる。ただし、

This function will be serialized, and then deserialized for injection.

ということで関数はシリアライズされるので、関数内でまた別の関数を呼ぼうとするとエラーになる。

その他ハマったポイント

ローカル開発中、ポップアップ起動のショートカットキー( _execute_action )の変更がなかなか反映されない、という謎の挙動に遭遇した。

何度か拡張の読み込みを試したり、 Chrome ごと再起動したら反映された。詳しい再現手順はわからず。

参考リンク