Quantcast RGPDソリューション「choice」とember.jsの競合

こんにちは、

これは非常に特定のバグですが、その影響はこの特定のケースを超えて広がる可能性があります。そのため、これについて質問があります。

(また、英語が拙いことをお詫びします。私はフランス人であり、ネイティブスピーカーではありません…)

まず、背景を説明させてください。
私はフランス語のラズベリーパイに関するフォーラム(forum.raspberry-pi.fr)で、しばらくDiscourseを利用しています。このフォーラムでは広告管理(themoneytizer)を使用しています。ご存知の通り、欧州ではユーザーのプライバシー保護のためにGDPR(欧州一般データ保護規則)の実装が義務付けられています。GDPR同意の主要な事業者(少なくともフランスでは)はQuantcastとその「Choice」ソリューションです。

そのため、私は以前からQuantcastのソリューションを使用しており、特に問題はありませんでしたが、最近「すべて同意」ボタンが正常に動作しなくなっていることに気づきました。クリックしても何も起こらず、開発者コンソールを確認すると以下のエラーが表示されました:「Uncaught TypeError: can’t define property “status”: Function is not extensible」。

私の理解する限り、何が起きているかというと:
この問題の根源を見つけるのに、信じられないほど長い時間がかかりました。どうやら、私があまり詳しくないEmber.jsが、Array、String、FunctionといったJavaScriptのネイティブオブジェクトを拡張しているようです。そして、何らかの理由で、これらのオブジェクトの拡張をある程度阻止しているように見えます(この部分はまだ完全に理解できていません)。

一方、Quantcastのソリューションは、おそらくFunctionAcceptAllという関数(このバグが「すべて同意」ボタンと「すべて拒否」ボタンのクリック時のみ発生する理由)で、オブジェクト(おそらく配列)を拡張しようとしています。しかし、そのオブジェクトの通常の動作はEmber.jsによって変更されているようです。

このバグを理解するための多くの調査の結果、Ember.jsの動作を変更してJavaScriptのプロトタイプを拡張しないようにすることが可能であることもわかりました。詳しくは以下のページをご覧ください:https://guides.emberjs.com/release/configuring-ember/disabling-prototype-extensions/

いくつかテストを行ってみたところ、window.EmberENV.FORCE_JQUERY = true; の行の後に window.EmberENV.EXTEND_PROTOTYPES = {String: true, Array: false}; という行をファイル _ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js に追加すると、バグが消えました。

試してみたい方のために、フォーラムの /tst/index.html ページをご覧ください(スクリプトが開始するにはヨーロッパのIPアドレスが必要かもしれません。私はわかりません)。
これで、私が提供できる情報はすべて揃ったと思います。

さて、ここが私の質問です。
このバグは非常に特定のケースですが、GDPRは現在ヨーロッパでますます普及しており、今後も簡単にはならないでしょう。
Quantcastは、GDPRの実装に数百ドルを支払う余裕がない事業者にとって、少なくともある程度独占的な地位にあります。このバグにより、Quantcastのあらゆる使用が不可能となり、結果としてヨーロッパにおけるDiscourseでの広告運用が阻害されます。これは大きな問題だと考えています。
また、私がこのバグをQuantcastで発見しただけであっても、広告やその他の目的で埋め込む必要がある多くのサードパーティスクリプト(これらは完全に制御不能であり、Array、String、Functionオブジェクトの「通常の」JavaScript動作に依存しています)でも同様のバグが発生する可能性があります。

Discourseのコードについては十分に理解していないため、お伺いします。Ember.jsがArray、String、Functionオブジェクトに追加するプロパティ(前述のリンク参照)は、Discourseで使用されていますか?もし使用されていないなら、このような副作用を防ぐために、Ember.jsのプロトタイプ拡張を無効にすることを検討すべきではないでしょうか?

どなたかこの件について情報をお寄せいただければ幸いです。
ありがとうございます。

「いいね!」 5

現時点ではこれはサポートされていません。拡張機能に依存しています。将来的なバージョンでは不要になる可能性もありますが、@eviltrout がより詳しい背景を説明できます。

ここでどうすべきか確信が持てませんが、何らかの回避策を用意すべきだと考えます。RGPD が機能しなくなることに驚きました。Quantcast にチケットを作成して議論し、ここにリンクを貼ることを提案します。

「いいね!」 4

Quantcast に必要なスクリプトをどのように追加していますか?以下のように、src 属性を持つ外部スクリプトタグで追加していますか?

<script src="foo"></script>

それとも、以下のようにインラインで追加していますか?

<script>
  alert("Hello World!");
</script>

こんにちは、ご返信ありがとうございます。

@sam についてですが、このトピックを作成した当日に Quantcast にチケットを提出し、本日回答をいただきました。現在確認中とのことで、解決策が見つかることを願っています。「選択を承認」ボタンは正常に動作しているため、彼らにとって比較的簡単な修正だと思われます。Quantcast が Discourse や Ember.js を十分に広く利用されているソリューションとして認識し、専用の修正を提供してくれることを期待しています。

@Johani についてですが、Quantcast のスクリプト自体は私が直接追加しているわけではありません。TheMoneyTizer がスクリプトを提供しており、それが自動的に追加処理を行っています(TCF2 イベントの検出なども同様です)。必要であれば、こちらのスクリプトをご覧ください:こちら

このスクリプトは、ページの最初のスクリプトの前に新しい <script> 要素を追加するもののようです。確証はありませんが、Quantcast の公式ドキュメントの例でほぼ同じスクリプトを見た記憶があります。おそらく、Quantcast Choice の統合に広く使われているスクリプトなのでしょう。

お時間いただき、ありがとうございました。

配列プロトタイプの安全な削除は、当分見送られると思います。長年にわたりほとんど問題を起こさず、非常に便利な機能を提供してきました。

Quantcast に連絡して、配列プロトタイプに依存しないスクリプトのバージョンを取得することはできませんか?

「いいね!」 2

みなさん、こんにちは!
新しい情報をお届けしますし、この問題の解決策も見つけました。

さて、今日もこのバグについて掘り下げてみたところ、さらに詳しい情報が得られたと思います。

Quantcast のファイル cmp2ui-fr.js を調べてみると、バグが発生している場所がわかりました。それは以下の関数の中です(現在は minified 版しかありません):

function(t){for(var n in t){t[n].status=e;}}

ご覧の通り、この関数は for..in ループを使用しており、変数 t は配列です。以前も説明した通り、Ember.js は JavaScript のネイティブな Array を拡張しています。その拡張の一つとして、_super というエントリが追加されていることがわかりました。

この _super エントリは、Ember.js 内の _utils.ROOT を参照していると思われる ROOT() 関数を指しています(この名前がどこかで見たことがある方もいるかもしれませんが、私には馴染みがありません ^^)。そして、この ROOT() 関数は拡張不可能(non-extensible)です。

どうやらこの _super プロパティは「列挙可能(enumerable)」とみなされているらしく、配列に対して for..in ループを実行すると、_super エントリが通常の要素として扱われてしまいます(例えば、valuesbindvalueOf など、配列オブジェクトの関数たちは例外ですが)。

これは Ember.js のバグであり、望ましい動作ではないと私は考えています。

さて、このバグを非常に再現しやすい形で再現させることができました。その方法は以下の通りです。まず、オブジェクトの配列を作成します:

var objs = [{'key':'val'},{'key':'val'}];

次に、厳密モード(strict mode)で配列をループし、各要素に新しいプロパティを設定する関数を作成します:

var tst_func = function (objs){'use strict';for(var i in objs){objs[i].newproperty = true }};

最後に、この関数を配列に対して呼び出します:

tst_func(objs);

すると、以下のようなエラーが発生します:
Uncaught TypeError: can't define property "newproperty": Function is not extensible

この現象を見ると、このバグはすでに広く存在しており、Quantcast に固有の問題ではない可能性が高いと考えられます。基本的に、for..in ループを使用する anyone は、一貫性のない動作や重大なバグに直面するリスクがあります。

私見では、これは Discourse や Quantcast の問題というよりも、明確に Ember.js の問題です。しかし、Ember.js で修正されるまで、何らかの対処法を見つける必要があるのも事実です ^^。

良い知らせとしては、この問題を修正する方法が見つかったと思います。

一つの方法は、すべての for..in ループ(おそらく forEachfor...of などにも同様に適用)に以下の行を追加することです:
if (!objs.hasOwnProperty(i)) {continue};
これにより、配列の _super の奇妙な動作自体は修正されませんが、ローカル的にはそのアクセスを防ぐことができます。ただし、これは当然ながら、Quantcast のような制御できない外部スクリプトには適用できません。

もう一つの方法(私が推奨するアプローチ)は、JavaScript の Array プロトタイプを変更することです(つまり、Ember.js によるオーバーライドをさらにオーバーライドする ^^)。これにより、_super を非列挙可能(non-enumerable)にします。そのためには、Ember.js が呼び出され評価された後に、以下の JavaScript 行を実行する必要があります:

//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, '_super', {'enumerable': false});

この方法では、_super の直接利用は本来の意図通り可能でありながら、ループ内での表示は防げます。ただし、この奇妙な動作が Ember.js の内部関数や外部プラグインで使用されていないとは断言できません。

ファイル _ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js を確認したところ、Discourse 固有の行がいくつか見つかりました:

var ALIASES = {
    "ember-addons/ember-computed-decorators":
      "discourse-common/utils/decorators",
    "discourse/lib/raw-templates": "discourse-common/lib/raw-templates",
    "preload-store": "discourse/lib/preload-store",
    "fixtures/user_fixtures": "discourse/tests/fixtures/user-fixtures",
  };
  var ALIAS_PREPEND = {
    fixtures: "discourse/tests/",
    helpers: "discourse/tests/",
  };

もしかしたら、ここで Object.defineProperty(Array.prototype, '_super', {'enumerable': false}); の行を追加できるかもしれません。

現時点では、私は以下の行を追加することでバグを修正しました:
//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, ‘_super’, {‘enumerable’: false});

これを Quantcast の choice.js スクリプトを呼び出す前に実行しています。

ローカルでこのバグを修正したい場合は、以下の 2 行を含む fix_ember.js というファイルを作成し、リバースプロキシ(例えば Nginx)から静的に提供し、テーマのフッターにカスタマイズで <script src="/fix_emmber.js"></script> の行を追加する方法があります。スクリプトを直接記述するのではなくリンクとして追加する必要があります。これは、Discourse のスクリプト抽出機能のためです(詳細は Custom javascript in <head> disappear を参照してください)。

このトピックが他の方のお役に立てれば幸いです。明日、Ember.js にチケットを開き、これがバグなのか、あるいは非常に奇妙だが意図された動作なのかを確認します。

修正を Discourse に含める必要があるかどうか、引き続き情報をお伝えします。

PS: Extending JavaScript Natives – JavaScript, JavaScript… の Angus Croll 氏に心から感謝します。彼の投稿が非常に役立ちました!

「いいね!」 3

@OsaAjani 様、この件にご対応いただき誠にありがとうございます。当方の Discourse フォーラムでも Quantcast Choice で全く同じ問題が発生しており、エラーを発生させない最後のバージョンであるバージョン 12 に留まらざるを得ない状況です。

最新バージョンは 23 ですが、ぜひ最新のバージョンを使用したいと考えております。

できるだけ早くこの問題を解決する方法についてご教示いただけますと幸いです。

@Terrapop さん、このトピックが役立つことを嬉しく思います。現時点では、私が ember.js リポジトリで開いた issue ([Bug] Property "_super" is enumerable on Array, creating conflicts with external libs (ex : Quantcast Choice RGPD) · Issue #19289 · emberjs/ember.js · GitHub) について全くフィードバックがありません。Quantcast からも同様です。

ただし、良い知らせもあります。前のメッセージで説明した修正を適用しています。fix_ember.js というファイルを作成し、以下のコードを記述しました:

Object.defineProperty(Array.prototype, '_super', {'enumerable': false});

そして、管理パネルのカスタマイズ機能を使ってテーマのフッターに以下の行を追加します:

<script defer src="/fix_emmber.js"></script>

この解決策は問題を解消しており、現時点では副作用は見つかっていません。したがって、この修正を安心して使用し、Quantcast の v23 へ移行していただくことができます。

「いいね!」 2

choice.js を読み込む前にその行を追加しました。これも問題なく動作しているようです。

もう一つの解決策についてですが、Discourse の前にリバースプロキシを置いていないため、fix_ember.js ファイルを適切に作成する方法がわかりません。Docker 内でもそれは可能でしょうか?ご提案があればお願いします。

まあ、choice.js より前に読み込めれば、さらに素晴らしいですね!

「いいね!」 1

はい、動作します。本当にありがとうございます。この問題の特定に数日間頭を悩ませていました。Quantcast と 20 通以上もメールのやり取りをしましたが、彼らも問題の特定ができませんでした。

さて、Quantcast とのコミュニケーション経路が良好であれば、この投稿を彼らに紹介して修正してもらうよう依頼できるかもしれません。私は連絡を取りましたが、実際には彼らのクライアントではないため、サポートからの返信はあまり得られませんでした。

承知しました。ご指摘の通りです。