Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Easily manage and trigger webhooks directly from your browser! Compatible with F
**🧪 Test a Webhook:**
- When adding or editing a webhook, click the 'Test' button to send a test payload to your URL.

**🕒 DateTime Variables in Custom Payload:**
- You can use `{{now.*}}` placeholders in custom payload JSON, including UTC values like `{{now.iso}}`, `{{now.date}}`, `{{now.time}}`, `{{now.unix}}`, and local values like `{{now.local.iso}}`.
- Existing `{{triggeredAt}}` remains available as an alias for `{{now.iso}}` for backward compatibility.

**🗂️ Organize into Groups:**
- Use the group management dialog to add, delete, rename, or reorder groups via drag-and-drop.

Expand Down
19 changes: 17 additions & 2 deletions options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,27 @@ <h2>__MSG_optionsAddWebhookHeader__</h2>
<li><code>{{tab.audible}}</code> - Is tab playing audio</li>
<li><code>{{tab.incognito}}</code> - Is tab in incognito mode</li>
<li><code>{{tab.status}}</code> - Tab loading status</li>
<li><code>{{platform.arch}}</code> - Plaform architecture</li>
<li><code>{{platform.arch}}</code> - Platform architecture</li>
<li><code>{{platform.os}}</code> - Operating system</li>
<li><code>{{platform.version}}</code> - Platform version</li>
<li><code>{{browser}}</code> - Browser information</li>
<li><code>{{platform}}</code> - Platform information</li>
<li><code>{{triggeredAt}}</code> - Timestamp when triggered</li>
<li><code>{{now.iso}}</code> - Current UTC time as ISO 8601</li>
<li><code>{{now.date}}</code> - Current UTC date (YYYY-MM-DD)</li>
<li><code>{{now.time}}</code> - Current UTC time (HH:mm:ss)</li>
<li><code>{{now.unix}}</code> - Current UTC Unix timestamp in seconds</li>
<li><code>{{now.unix_ms}}</code> - Current UTC Unix timestamp in milliseconds</li>
<li><code>{{now.year}}</code> - Current UTC year</li>
<li><code>{{now.month}}</code> - Current UTC month (1-12)</li>
<li><code>{{now.day}}</code> - Current UTC day of month (1-31)</li>
<li><code>{{now.hour}}</code> - Current UTC hour (0-23)</li>
<li><code>{{now.minute}}</code> - Current UTC minute (0-59)</li>
<li><code>{{now.second}}</code> - Current UTC second (0-59)</li>
<li><code>{{now.millisecond}}</code> - Current UTC millisecond (0-999)</li>
<li><code>{{now.local.iso}}</code> - Current local time as ISO 8601</li>
<li><code>{{now.local.date}}</code> - Current local date (YYYY-MM-DD)</li>
<li><code>{{now.local.time}}</code> - Current local time (HH:mm:ss)</li>
<li><code>{{triggeredAt}}</code> - Alias for <code>{{now.iso}}</code></li>
<li><code>{{identifier}}</code> - Custom identifier</li>
</ul>
</div>
Expand Down
4 changes: 4 additions & 0 deletions options/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,10 @@ const availableVariables = [
"{{tab.index}}", "{{tab.pinned}}", "{{tab.audible}}", "{{tab.incognito}}",
"{{tab.status}}", "{{browser}}", "{{platform}}", "{{triggeredAt}}", "{{identifier}}",
"{{platform.arch}}", "{{platform.os}}", "{{platform.version}}",
"{{now.iso}}", "{{now.date}}", "{{now.time}}", "{{now.unix}}", "{{now.unix_ms}}",
"{{now.year}}", "{{now.month}}", "{{now.day}}", "{{now.hour}}", "{{now.minute}}",
"{{now.second}}", "{{now.millisecond}}", "{{now.local.iso}}", "{{now.local.date}}",
"{{now.local.time}}",
];

// Implement autocompletion for custom payload
Expand Down
48 changes: 48 additions & 0 deletions tests/sendWebhook.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('sendWebhook', () => {
global.fetch = originalFetch;
global.browser = originalBrowser;
console.error = originalConsoleError;
jest.useRealTimers();
jest.clearAllMocks();
});

Expand Down Expand Up @@ -174,4 +175,51 @@ describe('sendWebhook', () => {
await expect(sendWebhook(webhook, false))
.rejects.toThrow('popupErrorCustomPayloadJsonParseError');
});

test('should replace all new now datetime placeholders', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z'));

const webhook = {
url: 'https://custom.com',
customPayload: '{"iso":"{{now.iso}}","date":"{{now.date}}","time":"{{now.time}}","unix":{{now.unix}},"unixMs":{{now.unix_ms}},"year":{{now.year}},"month":{{now.month}},"day":{{now.day}},"hour":{{now.hour}},"minute":{{now.minute}},"second":{{now.second}},"millisecond":{{now.millisecond}},"localIso":"{{now.local.iso}}","localDate":"{{now.local.date}}","localTime":"{{now.local.time}}"}'
};

await sendWebhook(webhook, false);

const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(fetchBody.iso).toBe('2025-08-07T10:20:30.123Z');
expect(fetchBody.date).toBe('2025-08-07');
expect(fetchBody.time).toBe('10:20:30');
expect(fetchBody.unix).toBe(1754562030);
expect(fetchBody.unixMs).toBe(1754562030123);
expect(fetchBody.year).toBe(2025);
expect(fetchBody.month).toBe(8);
expect(fetchBody.day).toBe(7);
expect(fetchBody.hour).toBe(10);
expect(fetchBody.minute).toBe(20);
expect(fetchBody.second).toBe(30);
expect(fetchBody.millisecond).toBe(123);
expect(fetchBody.localDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(fetchBody.localTime).toMatch(/^\d{2}:\d{2}:\d{2}$/);
expect(fetchBody.localIso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);

});

test('should keep triggeredAt as alias for now.iso', async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z'));

const webhook = {
url: 'https://custom.com',
customPayload: '{"now":"{{now.iso}}","legacy":"{{triggeredAt}}"}'
};

await sendWebhook(webhook, false);

const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(fetchBody.now).toBe('2025-08-07T10:20:30.123Z');
expect(fetchBody.legacy).toBe('2025-08-07T10:20:30.123Z');

});
});
56 changes: 52 additions & 4 deletions utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,52 @@ function replaceI18nPlaceholders() {
}
}

const padDatePart = (value, length = 2) => String(value).padStart(length, '0');

const toLocalIsoString = (date) => {
const year = date.getFullYear();
const month = padDatePart(date.getMonth() + 1);
const day = padDatePart(date.getDate());
const hour = padDatePart(date.getHours());
const minute = padDatePart(date.getMinutes());
const second = padDatePart(date.getSeconds());
const millisecond = padDatePart(date.getMilliseconds(), 3);
const offsetMinutes = -date.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? '+' : '-';
const absoluteOffset = Math.abs(offsetMinutes);
const offsetHour = padDatePart(Math.floor(absoluteOffset / 60));
const offsetMinute = padDatePart(absoluteOffset % 60);

return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}${sign}${offsetHour}:${offsetMinute}`;
};

const buildDateTimeVariables = (date = new Date()) => {
const nowIso = date.toISOString();
const timestampMs = date.getTime();

return {
nowIso,
values: {
"{{now.iso}}": nowIso,
"{{now.date}}": nowIso.slice(0, 10),
"{{now.time}}": nowIso.slice(11, 19),
"{{now.unix}}": Math.floor(timestampMs / 1000),
"{{now.unix_ms}}": timestampMs,
"{{now.year}}": date.getUTCFullYear(),
"{{now.month}}": date.getUTCMonth() + 1,
"{{now.day}}": date.getUTCDate(),
"{{now.hour}}": date.getUTCHours(),
"{{now.minute}}": date.getUTCMinutes(),
"{{now.second}}": date.getUTCSeconds(),
"{{now.millisecond}}": date.getUTCMilliseconds(),
"{{now.local.iso}}": toLocalIsoString(date),
"{{now.local.date}}": `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`,
"{{now.local.time}}": `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}:${padDatePart(date.getSeconds())}`,
"{{triggeredAt}}": nowIso,
},
};
};

async function sendWebhook(webhook, isTest = false) {
const browserAPI = getBrowserAPI();
let selectors = Array.isArray(webhook?.selectors) ? [...webhook.selectors] : [];
Expand All @@ -91,11 +137,13 @@ async function sendWebhook(webhook, isTest = false) {
try {
let payload;

const dateTimeVariables = buildDateTimeVariables();

if (isTest) {
payload = {
url: "https://example.com",
test: true,
triggeredAt: new Date().toISOString(),
triggeredAt: dateTimeVariables.nowIso,
};
} else {
// Get info about the active tab
Expand Down Expand Up @@ -185,7 +233,7 @@ async function sendWebhook(webhook, isTest = false) {
},
browser: browserInfo,
platform: platformInfo,
triggeredAt: new Date().toISOString(),
triggeredAt: dateTimeVariables.nowIso,
};

if (webhook && webhook.identifier) {
Expand All @@ -212,9 +260,9 @@ async function sendWebhook(webhook, isTest = false) {
"{{platform.arch}}": platformInfo.arch || "unknown",
"{{platform.os}}": platformInfo.os || "unknown",
"{{platform.version}}": platformInfo.version,
"{{triggeredAt}}": new Date().toISOString(),
"{{identifier}}": webhook.identifier || "",
"{{selectorContent}}": selectorContent
"{{selectorContent}}": selectorContent,
...dateTimeVariables.values,
};

let customPayloadStr = webhook.customPayload;
Expand Down