From 4fec5b203bf308057a06f9a20d3ba74b1ba5161b Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Fri, 11 Aug 2023 03:27:38 -0400 Subject: [PATCH 1/8] feat(commit-context): Do not create if older than 1 year --- .../integrations/utils/commit_context.py | 6 +- tests/sentry/tasks/test_commit_context.py | 69 ++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/sentry/integrations/utils/commit_context.py b/src/sentry/integrations/utils/commit_context.py index 0c9da764c7b033..fa6f853360b895 100644 --- a/src/sentry/integrations/utils/commit_context.py +++ b/src/sentry/integrations/utils/commit_context.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from datetime import datetime, timedelta, timezone from typing import Any, List, Mapping, Sequence, Tuple import sentry_sdk @@ -108,7 +109,10 @@ def find_commit_context_for_event( }, ) - if commit_context: + # Only return suspect commits that are less than a year old + if commit_context and datetime.strptime( + commit_context["committedDate"], "%Y-%m-%dT%H:%M:%SZ" + ).replace(tzinfo=timezone.utc) > datetime.now(tz=timezone.utc) - timedelta(days=365): result.append((commit_context, code_mapping)) return result, installation diff --git a/tests/sentry/tasks/test_commit_context.py b/tests/sentry/tasks/test_commit_context.py index 641633127c3b5a..a117304a97bf84 100644 --- a/tests/sentry/tasks/test_commit_context.py +++ b/tests/sentry/tasks/test_commit_context.py @@ -1,4 +1,5 @@ -from datetime import timedelta +from datetime import datetime, timedelta +from datetime import timezone as datetime_timezone from unittest.mock import Mock, patch import pytest @@ -83,7 +84,9 @@ class TestCommitContext(TestCommitContextMixin): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfwreqr", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -141,11 +144,51 @@ def test_failed_to_fetch_commit_context_record(self, mock_get_commit_context, mo error_message="integration_failed", ) + @patch("sentry.tasks.commit_context.logger") @patch( "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfasdf", - "committedDate": "2023-02-14T11:11Z", + "committedDate": ( + datetime.now(tz=datetime_timezone.utc) - timedelta(days=370) + ).strftime("%Y-%m-%dT%H:%M:%SZ"), + "commitMessage": "placeholder commit message", + "commitAuthorName": "", + "commitAuthorEmail": "admin@localhost", + }, + ) + def test_found_commit_is_too_old(self, mock_get_commit_context, mock_logger): + with self.tasks(): + assert not GroupOwner.objects.filter(group=self.event.group).exists() + event_frames = get_frame_paths(self.event) + process_commit_context( + event_id=self.event.event_id, + event_platform=self.event.platform, + event_frames=event_frames, + group_id=self.event.group_id, + project_id=self.event.project_id, + ) + + assert mock_logger.info.call_count == 1 + mock_logger.info.assert_called_with( + "process_commit_context.find_commit_context", + extra={ + "event": self.event.event_id, + "group": self.event.group_id, + "organization": self.event.group.project.organization_id, + "reason": "could_not_fetch_commit_context", + "code_mappings_count": 1, + "fallback": True, + }, + ) + + @patch( + "sentry.integrations.github.GitHubIntegration.get_commit_context", + return_value={ + "commitId": "asdfasdf", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -170,7 +213,9 @@ def test_no_matching_commit_in_db(self, mock_get_commit_context): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfwreqr", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -255,7 +300,9 @@ def test_no_inapp_frame_in_stacktrace(self, mock_process_suspect_commits): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -296,7 +343,9 @@ def test_commit_author_not_in_sentry(self, mock_get_commit_context): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -337,7 +386,9 @@ def test_commit_author_no_user(self, mock_get_commit_context, mock_get_users_for "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -423,7 +474,9 @@ def after_return(self, status, retval, task_id, args, kwargs, einfo): Mock( return_value={ "commitId": "asdfwreqr", - "committedDate": "2023-02-14T11:11Z", + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", From b366a986455e832b3a60d5dfa8f850db851629cf Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Tue, 15 Aug 2023 12:08:18 -0400 Subject: [PATCH 2/8] ref date comparison into a separate function --- src/sentry/integrations/utils/commit_context.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/sentry/integrations/utils/commit_context.py b/src/sentry/integrations/utils/commit_context.py index fa6f853360b895..c6dfd728f3dba5 100644 --- a/src/sentry/integrations/utils/commit_context.py +++ b/src/sentry/integrations/utils/commit_context.py @@ -110,9 +110,13 @@ def find_commit_context_for_event( ) # Only return suspect commits that are less than a year old - if commit_context and datetime.strptime( - commit_context["committedDate"], "%Y-%m-%dT%H:%M:%SZ" - ).replace(tzinfo=timezone.utc) > datetime.now(tz=timezone.utc) - timedelta(days=365): + if commit_context and is_date_less_than_year(commit_context["committedDate"]): result.append((commit_context, code_mapping)) return result, installation + + +def is_date_less_than_year(date): + return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) > datetime.now(tz=timezone.utc) - timedelta(days=365) From 53a1f44be6793193b4f22b19423866a9154ab28a Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Tue, 15 Aug 2023 14:20:38 -0400 Subject: [PATCH 3/8] add dates to test fixture --- tests/sentry/tasks/test_post_process.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 4d74974e3d5020..d440bd430c949d 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -1296,7 +1296,9 @@ def test_issue_owners_should_ratelimit(self, logger): class ProcessCommitsTestMixin(BasePostProgressGroupMixin): github_blame_return_value = { "commitId": "asdfwreqr", - "committedDate": "", + "committedDate": (datetime.now(timezone.utc) - timedelta(days=2)).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", From 704f9228d4ec02be756a4343f94501a9277a923c Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Wed, 16 Aug 2023 14:28:43 -0400 Subject: [PATCH 4/8] return datetime objects to make comparisons easy --- src/sentry/integrations/github/integration.py | 11 ++++++++--- src/sentry/integrations/gitlab/integration.py | 9 ++++----- src/sentry/integrations/utils/commit_context.py | 6 ++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/sentry/integrations/github/integration.py b/src/sentry/integrations/github/integration.py index 41d8cabbe74d5e..fb391e3ea53161 100644 --- a/src/sentry/integrations/github/integration.py +++ b/src/sentry/integrations/github/integration.py @@ -2,7 +2,7 @@ import logging import re -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Collection, Dict, Mapping, Sequence from django.http import HttpResponse @@ -242,6 +242,7 @@ def get_commit_context( except ApiError as e: raise e + date_format_expected = "%Y-%m-%dT%H:%M:%SZ" try: commit: Mapping[str, Any] = max( ( @@ -250,7 +251,7 @@ def get_commit_context( if blame.get("startingLine", 0) <= lineno <= blame.get("endingLine", 0) ), key=lambda blame: datetime.strptime( - blame.get("commit", {}).get("committedDate"), "%Y-%m-%dT%H:%M:%SZ" + blame.get("commit", {}).get("committedDate"), date_format_expected ), default={}, ) @@ -263,9 +264,13 @@ def get_commit_context( if not commitInfo: return None else: + committed_date = datetime.strptime( + commitInfo.get("committed_date"), date_format_expected + ).astimezone(timezone.utc) + return { "commitId": commitInfo.get("oid"), - "committedDate": commitInfo.get("committedDate"), + "committedDate": committed_date, "commitMessage": commitInfo.get("message"), "commitAuthorName": commitInfo.get("author", {}).get("name"), "commitAuthorEmail": commitInfo.get("author", {}).get("email"), diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index e6d647250dd132..000728ddfd37af 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -181,11 +181,10 @@ def get_commit_context( if not commitInfo: return None else: - committed_date = "{}Z".format( - datetime.strptime(commitInfo.get("committed_date"), date_format_expected) - .astimezone(timezone.utc) - .strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] - ) + committed_date = datetime.strptime( + commitInfo.get("committed_date"), date_format_expected + ).astimezone(timezone.utc) + return { "commitId": commitInfo.get("id"), "committedDate": committed_date, diff --git a/src/sentry/integrations/utils/commit_context.py b/src/sentry/integrations/utils/commit_context.py index c6dfd728f3dba5..e556447d825d85 100644 --- a/src/sentry/integrations/utils/commit_context.py +++ b/src/sentry/integrations/utils/commit_context.py @@ -116,7 +116,5 @@ def find_commit_context_for_event( return result, installation -def is_date_less_than_year(date): - return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").replace( - tzinfo=timezone.utc - ) > datetime.now(tz=timezone.utc) - timedelta(days=365) +def is_date_less_than_year(date: datetime): + return date > datetime.now(tz=timezone.utc) - timedelta(days=365) From 2e4e0fcf52c28cd297597b4c9705f758a6b49943 Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Wed, 16 Aug 2023 14:39:55 -0400 Subject: [PATCH 5/8] update test fixtures --- tests/sentry/tasks/test_commit_context.py | 32 ++++++----------------- tests/sentry/tasks/test_post_process.py | 4 +-- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/tests/sentry/tasks/test_commit_context.py b/tests/sentry/tasks/test_commit_context.py index a117304a97bf84..f45111178dd884 100644 --- a/tests/sentry/tasks/test_commit_context.py +++ b/tests/sentry/tasks/test_commit_context.py @@ -84,9 +84,7 @@ class TestCommitContext(TestCommitContextMixin): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfwreqr", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -149,9 +147,7 @@ def test_failed_to_fetch_commit_context_record(self, mock_get_commit_context, mo "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfasdf", - "committedDate": ( - datetime.now(tz=datetime_timezone.utc) - timedelta(days=370) - ).strftime("%Y-%m-%dT%H:%M:%SZ"), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=370)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -186,9 +182,7 @@ def test_found_commit_is_too_old(self, mock_get_commit_context, mock_logger): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfasdf", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -213,9 +207,7 @@ def test_no_matching_commit_in_db(self, mock_get_commit_context): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "asdfwreqr", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", @@ -300,9 +292,7 @@ def test_no_inapp_frame_in_stacktrace(self, mock_process_suspect_commits): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -343,9 +333,7 @@ def test_commit_author_not_in_sentry(self, mock_get_commit_context): "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -386,9 +374,7 @@ def test_commit_author_no_user(self, mock_get_commit_context, mock_get_users_for "sentry.integrations.github.GitHubIntegration.get_commit_context", return_value={ "commitId": "somekey", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "randomuser@sentry.io", @@ -474,9 +460,7 @@ def after_return(self, status, retval, task_id, args, kwargs, einfo): Mock( return_value={ "commitId": "asdfwreqr", - "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index d440bd430c949d..660aef4320ebae 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -1296,9 +1296,7 @@ def test_issue_owners_should_ratelimit(self, logger): class ProcessCommitsTestMixin(BasePostProgressGroupMixin): github_blame_return_value = { "commitId": "asdfwreqr", - "committedDate": (datetime.now(timezone.utc) - timedelta(days=2)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ), + "committedDate": (datetime.now(timezone.utc) - timedelta(days=2)), "commitMessage": "placeholder commit message", "commitAuthorName": "", "commitAuthorEmail": "admin@localhost", From b4420233076fd67ecd049c1d3f549b4d0b5adb6e Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Wed, 16 Aug 2023 14:59:10 -0400 Subject: [PATCH 6/8] add comment --- src/sentry/integrations/gitlab/integration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index 000728ddfd37af..0ae02416c17782 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -181,6 +181,7 @@ def get_commit_context( if not commitInfo: return None else: + # TODO(nisanthan): Use dateutil.parser.isoparse once on python 3.11 committed_date = datetime.strptime( commitInfo.get("committed_date"), date_format_expected ).astimezone(timezone.utc) From ab340d790b49dab3f0ce4522a348332c501d3865 Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Wed, 16 Aug 2023 17:33:42 -0400 Subject: [PATCH 7/8] use isodate.parse_datetime --- src/sentry/integrations/github/integration.py | 14 ++++++-------- src/sentry/integrations/gitlab/integration.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/sentry/integrations/github/integration.py b/src/sentry/integrations/github/integration.py index fb391e3ea53161..ba3a67706bf63f 100644 --- a/src/sentry/integrations/github/integration.py +++ b/src/sentry/integrations/github/integration.py @@ -2,12 +2,13 @@ import logging import re -from datetime import datetime, timezone +from datetime import timezone from typing import Any, Collection, Dict, Mapping, Sequence from django.http import HttpResponse from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ +from isodate import parse_datetime from rest_framework.request import Request from sentry import features, options @@ -242,7 +243,6 @@ def get_commit_context( except ApiError as e: raise e - date_format_expected = "%Y-%m-%dT%H:%M:%SZ" try: commit: Mapping[str, Any] = max( ( @@ -250,9 +250,7 @@ def get_commit_context( for blame in blame_range if blame.get("startingLine", 0) <= lineno <= blame.get("endingLine", 0) ), - key=lambda blame: datetime.strptime( - blame.get("commit", {}).get("committedDate"), date_format_expected - ), + key=lambda blame: parse_datetime(blame.get("commit", {}).get("committedDate")), default={}, ) if not commit: @@ -264,9 +262,9 @@ def get_commit_context( if not commitInfo: return None else: - committed_date = datetime.strptime( - commitInfo.get("committed_date"), date_format_expected - ).astimezone(timezone.utc) + committed_date = parse_datetime(commitInfo.get("committed_date")).astimezone( + timezone.utc + ) return { "commitId": commitInfo.get("oid"), diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index 0ae02416c17782..88a3330a1de637 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -1,12 +1,13 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import timezone from typing import Any, Mapping, Sequence from urllib.parse import urlparse from django import forms from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ +from isodate import parse_datetime from rest_framework.request import Request from sentry.identity.gitlab import get_oauth_data, get_user_info @@ -166,13 +167,10 @@ def get_commit_context( except ApiError as e: raise e - date_format_expected = "%Y-%m-%dT%H:%M:%S.%f%z" try: commit = max( blame_range, - key=lambda blame: datetime.strptime( - blame.get("commit", {}).get("committed_date"), date_format_expected - ), + key=lambda blame: parse_datetime(blame.get("commit", {}).get("committed_date")), ) except (ValueError, IndexError): return None @@ -182,9 +180,9 @@ def get_commit_context( return None else: # TODO(nisanthan): Use dateutil.parser.isoparse once on python 3.11 - committed_date = datetime.strptime( - commitInfo.get("committed_date"), date_format_expected - ).astimezone(timezone.utc) + committed_date = parse_datetime(commitInfo.get("committed_date")).astimezone( + timezone.utc + ) return { "commitId": commitInfo.get("id"), From 81b93a7d969f739b0c527a18db06279dfd58d833 Mon Sep 17 00:00:00 2001 From: Nisanthan Nanthakumar Date: Wed, 16 Aug 2023 17:36:12 -0400 Subject: [PATCH 8/8] update test --- tests/sentry/integrations/gitlab/test_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/sentry/integrations/gitlab/test_integration.py b/tests/sentry/integrations/gitlab/test_integration.py index 978559fae4793f..bb539181a780bd 100644 --- a/tests/sentry/integrations/gitlab/test_integration.py +++ b/tests/sentry/integrations/gitlab/test_integration.py @@ -4,6 +4,7 @@ import responses from django.core.cache import cache from django.test import override_settings +from isodate import parse_datetime from fixtures.gitlab import GET_COMMIT_RESPONSE, GitLabTestCase from sentry.integrations.gitlab import GitlabIntegrationProvider @@ -427,7 +428,7 @@ def test_get_commit_context(self): commit_context_expected = { "commitId": "d42409d56517157c48bf3bd97d3f75974dde19fb", - "committedDate": "2015-12-18T08:12:22.000Z", + "committedDate": parse_datetime("2015-12-18T08:12:22.000Z"), "commitMessage": "Add installation instructions", "commitAuthorName": "Nisanthan Nanthakumar", "commitAuthorEmail": "nisanthan.nanthakumar@sentry.io",