Repository: electron/electron
Pull request: fix: wrong cause and removed flag in cookie change listener

#### Description of Change

Closes https://github.com/electron/electron/issues/49102.

Fixes an issue where cookie changed events weren't properly emitted in all cases, as Electron only checked for `net::CookieChangeCause::INSERTED` and not `net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE`.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where cookie changed events weren't properly emitted in all cases.

Changed files:
filename: docs/api/cookies.md
patch: @@ -51,7 +51,12 @@ Returns:
 * `event` Event
 * `cookie` [Cookie](structures/cookie.md) - The cookie that was changed.
 * `cause` string - The cause of the change with one of the following values:
-  * `explicit` - The cookie was changed directly by a consumer's action.
+  * `inserted` -  The cookie was inserted.
+  * `inserted-no-change-overwrite` - The newly inserted cookie overwrote a cookie but
+    did not result in any change. For example, inserting an identical cookie will produce this cause.
+  * `inserted-no-value-change-overwrite` - The newly inserted cookie overwrote a cookie but
+    did not result in any value change, but it's web observable (e.g. updates the expiry).
+  * `explicit` - The cookie was deleted directly by a consumer's action.
   * `overwrite` - The cookie was automatically removed due to an insert
     operation that overwrote it.
   * `expired` - The cookie was automatically removed as it expired.

filename: docs/breaking-changes.md
patch: @@ -30,6 +30,14 @@ Previously, PDF resources created a separate guest [WebContents](https://www.ele
 
 Under the hood, Chromium [enabled](https://chromium-review.googlesource.com/c/chromium/src/+/7239572) a feature that changes PDFs to use out-of-process iframes (OOPIFs) instead of the `MimeHandlerViewGuest` extension.
 
+### Behavior Changed: Updated Cookie Change Cause in the Cookie 'changed' Event
+
+We have updated the [cookie](https://www.electronjs.org/docs/latest/api/cookies#event-changed) change cause in the cookie 'changed' event.
+When a new cookie is set, the change cause is `inserted`.
+When a cookie is deleted, the change cause remains `explicit`.
+When the cookie being set is identical to an existing one (same name, domain, path, and value, with no actual changes), the change cause is `inserted-no-change-overwrite`.
+When the value of the cookie being set remains unchanged but some of its attributes are updated, such as the expiration attribute, the change cause will be `inserted-no-value-change-overwrite`.
+
 ## Planned Breaking API Changes (40.0)
 
 ### Deprecated: `clipboard` API access from renderer processes

filename: shell/browser/api/electron_api_cookies.cc
patch: @@ -80,6 +80,11 @@ struct Converter<net::CookieChangeCause> {
                                    const net::CookieChangeCause& val) {
     switch (val) {
       case net::CookieChangeCause::INSERTED:
+        return gin::StringToV8(isolate, "inserted");
+      case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
+        return gin::StringToV8(isolate, "inserted-no-change-overwrite");
+      case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
+        return gin::StringToV8(isolate, "inserted-no-value-change-overwrite");
       case net::CookieChangeCause::EXPLICIT:
         return gin::StringToV8(isolate, "explicit");
       case net::CookieChangeCause::OVERWRITE:
@@ -272,6 +277,17 @@ std::string StringToCookieSameSite(const std::string* str_ptr,
   return "";
 }
 
+bool IsDeletion(net::CookieChangeCause cause) {
+  switch (cause) {
+    case net::CookieChangeCause::INSERTED:
+    case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
+    case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
+      return false;
+    default:
+      return true;
+  }
+}
+
 }  // namespace
 
 gin::DeprecatedWrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
@@ -435,10 +451,10 @@ v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
 void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
   v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
   v8::HandleScope scope(isolate);
+  bool is_deletion = IsDeletion(change.cause);
   Emit("changed", gin::ConvertToV8(isolate, change.cookie),
        gin::ConvertToV8(isolate, change.cause),
-       gin::ConvertToV8(isolate,
-                        change.cause != net::CookieChangeCause::INSERTED));
+       gin::ConvertToV8(isolate, is_deletion));
 }
 
 // static

filename: spec/api-session-spec.ts
patch: @@ -214,7 +214,7 @@ describe('session module', () => {
 
       expect(setEventCookie.name).to.equal(name);
       expect(setEventCookie.value).to.equal(value);
-      expect(setEventCause).to.equal('explicit');
+      expect(setEventCause).to.equal('inserted');
       expect(setEventRemoved).to.equal(false);
 
       expect(removeEventCookie.name).to.equal(name);