Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
gitlab-ce
Commits
7e0ef892
Commit
7e0ef892
authored
Apr 20, 2016
by
Alfredo Sumaran
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into label-dropdown-fix
parents
a5290ac2
7e6d5906
Changes
58
Hide whitespace changes
Inline
Side-by-side
Showing
58 changed files
with
3448 additions
and
191 deletions
+3448
-191
CHANGELOG
CHANGELOG
+15
-2
CONTRIBUTING.md
CONTRIBUTING.md
+1
-0
app/assets/javascripts/application.js.coffee
app/assets/javascripts/application.js.coffee
+1
-0
app/assets/javascripts/dispatcher.js.coffee
app/assets/javascripts/dispatcher.js.coffee
+2
-1
app/assets/javascripts/due_date_select.js.coffee
app/assets/javascripts/due_date_select.js.coffee
+64
-0
app/assets/javascripts/gl_dropdown.js.coffee
app/assets/javascripts/gl_dropdown.js.coffee
+4
-4
app/assets/javascripts/issuable.js.coffee
app/assets/javascripts/issuable.js.coffee
+84
-0
app/assets/javascripts/issuable_context.js.coffee
app/assets/javascripts/issuable_context.js.coffee
+0
-1
app/assets/javascripts/issues.js.coffee
app/assets/javascripts/issues.js.coffee
+1
-50
app/assets/javascripts/labels_select.js.coffee
app/assets/javascripts/labels_select.js.coffee
+44
-19
app/assets/javascripts/lib/animate.js.coffee
app/assets/javascripts/lib/animate.js.coffee
+30
-4
app/assets/javascripts/lib/url_utility.js.coffee
app/assets/javascripts/lib/url_utility.js.coffee
+14
-2
app/assets/javascripts/merge_requests.js.coffee
app/assets/javascripts/merge_requests.js.coffee
+0
-35
app/assets/javascripts/milestone_select.js.coffee
app/assets/javascripts/milestone_select.js.coffee
+1
-1
app/assets/javascripts/raven_config.js.coffee
app/assets/javascripts/raven_config.js.coffee
+44
-0
app/assets/javascripts/users_select.js.coffee
app/assets/javascripts/users_select.js.coffee
+1
-1
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+128
-2
app/assets/stylesheets/framework/markdown_area.scss
app/assets/stylesheets/framework/markdown_area.scss
+9
-0
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+5
-0
app/assets/stylesheets/pages/detail_page.scss
app/assets/stylesheets/pages/detail_page.scss
+6
-0
app/assets/stylesheets/pages/diff.scss
app/assets/stylesheets/pages/diff.scss
+3
-2
app/assets/stylesheets/pages/help.scss
app/assets/stylesheets/pages/help.scss
+2
-0
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+3
-2
app/assets/stylesheets/pages/note_form.scss
app/assets/stylesheets/pages/note_form.scss
+0
-12
app/assets/stylesheets/pages/notes.scss
app/assets/stylesheets/pages/notes.scss
+4
-17
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+4
-3
app/controllers/projects/merge_requests_controller.rb
app/controllers/projects/merge_requests_controller.rb
+3
-2
app/finders/issuable_finder.rb
app/finders/issuable_finder.rb
+40
-1
app/helpers/application_helper.rb
app/helpers/application_helper.rb
+8
-1
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+19
-0
app/helpers/issues_helper.rb
app/helpers/issues_helper.rb
+13
-1
app/helpers/sorting_helper.rb
app/helpers/sorting_helper.rb
+18
-0
app/models/issue.rb
app/models/issue.rb
+27
-0
app/models/label.rb
app/models/label.rb
+4
-0
app/views/projects/_activity.html.haml
app/views/projects/_activity.html.haml
+4
-1
app/views/projects/issues/_issue.html.haml
app/views/projects/issues/_issue.html.haml
+5
-0
app/views/projects/merge_requests/show/_mr_title.html.haml
app/views/projects/merge_requests/show/_mr_title.html.haml
+2
-2
app/views/shared/_label_row.html.haml
app/views/shared/_label_row.html.haml
+1
-1
app/views/shared/_labels_row.html.haml
app/views/shared/_labels_row.html.haml
+3
-0
app/views/shared/_sort_dropdown.html.haml
app/views/shared/_sort_dropdown.html.haml
+5
-0
app/views/shared/issuable/_filter.html.haml
app/views/shared/issuable/_filter.html.haml
+5
-3
app/views/shared/issuable/_label_dropdown.html.haml
app/views/shared/issuable/_label_dropdown.html.haml
+5
-3
app/views/shared/issuable/_nav.html.haml
app/views/shared/issuable/_nav.html.haml
+5
-5
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+34
-5
db/migrate/20160310124959_add_due_date_to_issues.rb
db/migrate/20160310124959_add_due_date_to_issues.rb
+6
-0
db/schema.rb
db/schema.rb
+2
-0
features/project/issues/filter_labels.feature
features/project/issues/filter_labels.feature
+1
-0
features/steps/project/issues/filter_labels.rb
features/steps/project/issues/filter_labels.rb
+4
-0
lib/api/internal.rb
lib/api/internal.rb
+1
-1
lib/gitlab/gon_helper.rb
lib/gitlab/gon_helper.rb
+1
-0
lib/gitlab/metrics.rb
lib/gitlab/metrics.rb
+9
-0
spec/features/issues/filter_by_labels_spec.rb
spec/features/issues/filter_by_labels_spec.rb
+172
-0
spec/features/issues/filter_issues_spec.rb
spec/features/issues/filter_issues_spec.rb
+8
-1
spec/features/issues_spec.rb
spec/features/issues_spec.rb
+92
-6
spec/features/merge_requests/filter_by_milestone_spec.rb
spec/features/merge_requests/filter_by_milestone_spec.rb
+6
-0
spec/finders/issues_finder_spec.rb
spec/finders/issues_finder_spec.rb
+16
-0
spec/lib/gitlab/metrics_spec.rb
spec/lib/gitlab/metrics_spec.rb
+24
-0
vendor/assets/javascripts/raven.js
vendor/assets/javascripts/raven.js
+2435
-0
No files found.
CHANGELOG
View file @
7e0ef892
...
...
@@ -19,6 +19,7 @@ v 8.7.0 (unreleased)
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- Restrict user profiles when public visibility level is restricted.
- Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524
...
...
@@ -86,6 +87,10 @@ v 8.7.0 (unreleased)
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- Show JavaScript errors in sentry
- Diff design updates (colors, button styles, etc)
- Copying and pasting a diff no longer pastes the line numbers or +/-
- Add null check to formData when updating profile content to fix Firefox bug
- Delete tags using Rugged for performance reasons (Robert Schilling)
- Add Slack notifications when Wiki is edited (Sebastian Klier)
- Diffs load at the correct point when linking from from number
...
...
@@ -107,7 +112,7 @@ v 8.7.0 (unreleased)
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
v 8.6.7
(unreleased)
v 8.6.7
- Fix persistent XSS vulnerability in `commit_person_link` helper
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
- Fix vulnerability that made it possible to enumerate private projects belonging to group
...
...
@@ -117,7 +122,6 @@ v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
...
...
@@ -249,6 +253,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
v 8.5.11
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.5.10
- Fix a 2FA authentication spoofing vulnerability.
...
...
@@ -396,6 +403,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
v 8.4.9
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.4.8
- Fix a 2FA authentication spoofing vulnerability.
...
...
@@ -518,6 +528,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.8
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.3.7
- Fix a 2FA authentication spoofing vulnerability.
...
...
CONTRIBUTING.md
View file @
7e0ef892
...
...
@@ -323,6 +323,7 @@ request is as follows:
[
shell command guidelines
](
doc/development/shell_commands.md
)
1.
If your code creates new files on disk please read the
[
shared files guidelines
](
doc/development/shared_files.md
)
.
1.
When writing commit messages please follow
[
these
](
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
)
[
guidelines
]
(http://chris.beams.io/posts/git-commit/).
The
**official merge window**
is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
...
...
app/assets/javascripts/application.js.coffee
View file @
7e0ef892
...
...
@@ -55,6 +55,7 @@
#= require_tree .
#= require fuzzaldrin-plus
#= require cropper
#= require raven
window
.
slugify
=
(
text
)
->
text
.
replace
(
/[^-a-zA-Z0-9]+/g
,
'_'
).
toLowerCase
()
...
...
app/assets/javascripts/dispatcher.js.coffee
View file @
7e0ef892
...
...
@@ -17,6 +17,7 @@ class Dispatcher
switch
page
when
'projects:issues:index'
Issues
.
init
()
Issuable
.
init
()
shortcut_handler
=
new
ShortcutsNavigation
()
when
'projects:issues:show'
new
Issue
()
...
...
@@ -57,7 +58,7 @@ class Dispatcher
new
ZenMode
()
when
'projects:merge_requests:index'
shortcut_handler
=
new
ShortcutsNavigation
()
MergeRequests
.
init
()
Issuable
.
init
()
when
'dashboard:activity'
new
Activities
()
when
'dashboard:projects:starred'
...
...
app/assets/javascripts/due_date_select.js.coffee
0 → 100644
View file @
7e0ef892
class
@
DueDateSelect
constructor
:
->
$loading
=
$
(
'.js-issuable-update .due_date'
)
.
find
(
'.block-loading'
)
.
hide
()
$
(
'.js-due-date-select'
).
each
(
i
,
dropdown
)
->
$dropdown
=
$
(
dropdown
)
$dropdownParent
=
$dropdown
.
closest
(
'.dropdown'
)
$datePicker
=
$dropdownParent
.
find
(
'.js-due-date-calendar'
)
$block
=
$dropdown
.
closest
(
'.block'
)
$selectbox
=
$dropdown
.
closest
(
'.selectbox'
)
$value
=
$block
.
find
(
'.value'
)
$sidebarValue
=
$
(
'.js-due-date-sidebar-value'
,
$block
)
fieldName
=
$dropdown
.
data
(
'field-name'
)
abilityName
=
$dropdown
.
data
(
'ability-name'
)
issueUpdateURL
=
$dropdown
.
data
(
'issue-update'
)
$dropdown
.
glDropdown
(
hidden
:
->
$selectbox
.
hide
()
$value
.
removeAttr
(
'style'
)
)
addDueDate
=
->
# Create the post date
value
=
$
(
"input[name='
#{
fieldName
}
']"
).
val
()
date
=
new
Date
value
.
replace
(
new
RegExp
(
'-'
,
'g'
),
','
)
mediumDate
=
$
.
datepicker
.
formatDate
'M d, yy'
,
date
data
=
{}
data
[
abilityName
]
=
{}
data
[
abilityName
].
due_date
=
value
$
.
ajax
(
type
:
'PUT'
url
:
issueUpdateURL
data
:
data
beforeSend
:
->
$loading
.
fadeIn
()
$dropdown
.
trigger
(
'loading.gl.dropdown'
)
$selectbox
.
hide
()
$value
.
removeAttr
(
'style'
)
$value
.
html
(
mediumDate
)
$sidebarValue
.
html
(
mediumDate
)
).
done
(
data
)
->
$dropdown
.
trigger
(
'loaded.gl.dropdown'
)
$dropdown
.
dropdown
(
'toggle'
)
$loading
.
fadeOut
()
$datePicker
.
datepicker
(
dateFormat
:
'yy-mm-dd'
,
defaultDate
:
$
(
"input[name='
#{
fieldName
}
']"
).
val
()
altField
:
"input[name='
#{
fieldName
}
']"
onSelect
:
->
addDueDate
()
)
$
(
document
)
.
off
'click'
,
'.ui-datepicker-header a'
.
on
'click'
,
'.ui-datepicker-header a'
,
(
e
)
->
e
.
stopImmediatePropagation
()
app/assets/javascripts/gl_dropdown.js.coffee
View file @
7e0ef892
...
...
@@ -389,13 +389,13 @@ class GitLabDropdown
else
selectedObject
else
if
!
value
?
field
.
remove
()
if
not
@
options
.
multiSelect
if
not
@
options
.
multiSelect
or
el
.
hasClass
(
'dropdown-clear-active'
)
@
dropdown
.
find
(
".
#{
ACTIVE_CLASS
}
"
).
removeClass
ACTIVE_CLASS
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
).
remove
()
if
!
value
?
field
.
remove
()
# Toggle active class for the tick mark
el
.
addClass
ACTIVE_CLASS
...
...
app/assets/javascripts/issuable.js.coffee
0 → 100644
View file @
7e0ef892
@
Issuable
=
init
:
->
Issuable
.
initTemplates
()
Issuable
.
initSearch
()
initTemplates
:
->
Issuable
.
labelRow
=
_
.
template
(
'<% _.each(labels, function(label){ %>
<span class="label-row">
<a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
</span>
<% }); %>'
)
initSearch
:
->
@
timer
=
null
$
(
'#issue_search'
)
.
off
'keyup'
.
on
'keyup'
,
->
clearTimeout
(
@
timer
)
@
timer
=
setTimeout
(
->
Issuable
.
filterResults
$
(
'#issue_search_form'
)
,
500
)
toggleLabelFilters
:
->
$filteredLabels
=
$
(
'.filtered-labels'
)
if
$filteredLabels
.
find
(
'.label-row'
).
length
>
0
$filteredLabels
.
removeClass
(
'hidden'
)
else
$filteredLabels
.
addClass
(
'hidden'
)
filterResults
:
(
form
)
=>
formData
=
form
.
serialize
()
$
(
'.issues-holder, .merge-requests-holder'
).
css
(
'opacity'
,
'0.5'
)
formAction
=
form
.
attr
(
'action'
)
issuesUrl
=
formAction
issuesUrl
+=
(
"
#{
if
formAction
.
indexOf
(
'?'
)
<
0
then
'?'
else
'&'
}
"
)
issuesUrl
+=
formData
$
.
ajax
type
:
'GET'
url
:
formAction
data
:
formData
complete
:
->
$
(
'.issues-holder, .merge-requests-holder'
).
css
(
'opacity'
,
'1.0'
)
success
:
(
data
)
->
$
(
'.issues-holder, .merge-requests-holder'
).
html
(
data
.
html
)
# Change url so if user reload a page - search results are saved
history
.
replaceState
{
page
:
issuesUrl
},
document
.
title
,
issuesUrl
Issuable
.
reload
()
Issuable
.
updateStateFilters
()
$filteredLabels
=
$
(
'.filtered-labels'
)
if
typeof
Issuable
.
labelRow
is
'function'
$filteredLabels
.
html
(
Issuable
.
labelRow
(
data
))
Issuable
.
toggleLabelFilters
()
dataType
:
"json"
reload
:
->
if
Issues
.
created
Issues
.
initChecks
()
$
(
'#filter_issue_search'
).
val
(
$
(
'#issue_search'
).
val
())
updateStateFilters
:
->
stateFilters
=
$
(
'.issues-state-filters'
)
newParams
=
{}
paramKeys
=
[
'author_id'
,
'milestone_title'
,
'assignee_id'
,
'issue_search'
]
for
paramKey
in
paramKeys
newParams
[
paramKey
]
=
gl
.
utils
.
getParameterValues
(
paramKey
)[
0
]
or
''
if
stateFilters
.
length
stateFilters
.
find
(
'a'
).
each
->
initialUrl
=
gl
.
utils
.
removeParamQueryString
(
$
(
this
).
attr
(
'href'
),
'label_name[]'
)
labelNameValues
=
gl
.
utils
.
getParameterValues
(
'label_name[]'
)
if
labelNameValues
labelNameQueryString
=
(
"label_name[]=
#{
value
}
"
for
value
in
labelNameValues
).
join
(
'&'
)
newUrl
=
"
#{
gl
.
utils
.
mergeUrlParams
(
newParams
,
initialUrl
)
}
&
#{
labelNameQueryString
}
"
else
newUrl
=
gl
.
utils
.
mergeUrlParams
(
newParams
,
initialUrl
)
$
(
this
).
attr
'href'
,
newUrl
app/assets/javascripts/issuable_context.js.coffee
View file @
7e0ef892
...
...
@@ -33,7 +33,6 @@ class @IssuableContext
$block
.
find
(
'.dropdown-menu-toggle'
).
trigger
'click'
,
0
$
(
".right-sidebar"
).
niceScroll
()
initParticipants
:
->
...
...
app/assets/javascripts/issues.js.coffee
View file @
7e0ef892
@
Issues
=
init
:
->
Issues
.
initSearch
()
Issues
.
created
=
true
Issues
.
initChecks
()
$
(
"body"
).
on
"ajax:success"
,
".close_issue, .reopen_issue"
,
->
...
...
@@ -15,10 +15,6 @@
else
$
(
this
).
html
totalIssues
-
1
reload
:
->
Issues
.
initChecks
()
$
(
'#filter_issue_search'
).
val
(
$
(
'#issue_search'
).
val
())
initChecks
:
->
$
(
".check_all_issues"
).
click
->
$
(
".selected_issue"
).
prop
(
"checked"
,
@
checked
)
...
...
@@ -26,51 +22,6 @@
$
(
".selected_issue"
).
bind
"change"
,
Issues
.
checkChanged
# Update state filters if present in page
updateStateFilters
:
->
stateFilters
=
$
(
'.issues-state-filters'
)
newParams
=
{}
paramKeys
=
[
'author_id'
,
'label_name'
,
'milestone_title'
,
'assignee_id'
,
'issue_search'
]
for
paramKey
in
paramKeys
newParams
[
paramKey
]
=
gl
.
utils
.
getUrlParameter
(
paramKey
)
or
''
if
stateFilters
.
length
stateFilters
.
find
(
'a'
).
each
->
initialUrl
=
$
(
this
).
attr
'href'
$
(
this
).
attr
'href'
,
gl
.
utils
.
mergeUrlParams
(
newParams
,
initialUrl
)
# Make sure we trigger ajax request only after user stop typing
initSearch
:
->
@
timer
=
null
$
(
"#issue_search"
).
keyup
->
clearTimeout
(
@
timer
)
@
timer
=
setTimeout
(
->
Issues
.
filterResults
$
(
"#issue_search_form"
)
,
500
)
filterResults
:
(
form
)
=>
$
(
'.issues-holder, .merge-requests-holder'
).
css
(
"opacity"
,
'0.5'
)
formAction
=
form
.
attr
(
'action'
)
formData
=
form
.
serialize
()
issuesUrl
=
formAction
issuesUrl
+=
(
"
#{
if
formAction
.
indexOf
(
"?"
)
<
0
then
'?'
else
'&'
}
"
)
issuesUrl
+=
formData
$
.
ajax
type
:
"GET"
url
:
formAction
data
:
formData
complete
:
->
$
(
'.issues-holder, .merge-requests-holder'
).
css
(
"opacity"
,
'1.0'
)
success
:
(
data
)
->
$
(
'.issues-holder, .merge-requests-holder'
).
html
(
data
.
html
)
# Change url so if user reload a page - search results are saved
history
.
replaceState
{
page
:
issuesUrl
},
document
.
title
,
issuesUrl
Issues
.
reload
()
Issues
.
updateStateFilters
()
dataType
:
"json"
checkChanged
:
->
checked_issues
=
$
(
".selected_issue:checked"
)
if
checked_issues
.
length
>
0
...
...
app/assets/javascripts/labels_select.js.coffee
View file @
7e0ef892
...
...
@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl
=
$dropdown
.
data
(
'labels'
)
issueUpdateURL
=
$dropdown
.
data
(
'issueUpdate'
)
selectedLabel
=
$dropdown
.
data
(
'selected'
)
if
selectedLabel
?
if
selectedLabel
?
and
not
$dropdown
.
hasClass
'js-multiselect'
selectedLabel
=
selectedLabel
.
split
(
','
)
newLabelField
=
$
(
'#new_label_name'
)
newColorField
=
$
(
'#new_label_color'
)
...
...
@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName
=
$dropdown
.
data
(
'ability-name'
)
$selectbox
=
$dropdown
.
closest
(
'.selectbox'
)
$block
=
$selectbox
.
closest
(
'.block'
)
$form
=
$dropdown
.
closest
(
'form'
)
$sidebarCollapsedValue
=
$block
.
find
(
'.sidebar-collapsed-icon span'
)
$value
=
$block
.
find
(
'.value'
)
$newLabelError
=
$
(
'.js-label-error'
)
...
...
@@ -151,7 +152,7 @@ class @LabelsSelect
.
find
(
'a'
)
.
each
((
i
)
->
setTimeout
(
=>
gl
A
nimate
(
$
(
@
),
'pulse'
)
gl
.
animate
.
a
nimate
(
$
(
@
),
'pulse'
)
,
200
*
i
)
)
...
...
@@ -180,16 +181,21 @@ class @LabelsSelect
callback
data
renderRow
:
(
label
)
->
selectedClass
=
''
if
$selectbox
.
find
(
"input[type='hidden']
\
[name='
#{
$dropdown
.
data
(
'field-name'
)
}
']
\
[value='
#{
label
.
id
}
']"
).
length
selectedClass
=
'is-active'
removesAll
=
label
.
id
is
0
or
not
label
.
id
?
selectedClass
=
[]
if
$form
.
find
(
"input[type='hidden']
\
[name='
#{
$dropdown
.
data
(
'fieldName'
)
}
']
\
[value='
#{
this
.
id
(
label
)
}
']"
).
length
selectedClass
.
push
'is-active'
if
$dropdown
.
hasClass
(
'js-multiselect'
)
and
removesAll
selectedClass
.
push
'dropdown-clear-active'
color
=
if
label
.
color
?
then
"<span class='dropdown-label-box' style='background-color:
#{
label
.
color
}
'></span>"
else
""
"<li>
<a href='#' class='
#{
selectedClass
}
'>
<a href='#' class='
#{
selectedClass
.
join
(
' '
)
}
'>
#{
color
}
#{
_
.
escape
(
label
.
title
)
}
</a>
...
...
@@ -199,37 +205,56 @@ class @LabelsSelect
fields
:
[
'title'
]
selectable
:
true
toggleLabel
:
(
selected
)
->
toggleLabel
:
(
selected
,
el
)
->
selected_labels
=
$
(
'.js-label-select'
).
siblings
(
'.dropdown-menu-labels'
).
find
(
'.is-active'
)
if
selected
and
selected
.
title
?
selected
.
title
if
selected_labels
.
length
>
1
"
#{
selected
.
title
}
+
#{
selected_labels
.
length
-
1
}
more"
else
selected
.
title
else
if
not
selected
and
selected_labels
.
length
isnt
0
if
selected_labels
.
length
>
1
"
#{
$
(
selected_labels
[
0
]).
text
()
}
+
#{
selected_labels
.
length
-
1
}
more"
else
if
selected_labels
.
length
is
1
$
(
selected_labels
).
text
()
else
defaultLabel
fieldName
:
$dropdown
.
data
(
'field-name'
)
id
:
(
label
)
->
if
label
.
isAny
?
''
else
if
$dropdown
.
hasClass
"js-filter-submit"
if
$dropdown
.
hasClass
(
"js-filter-submit"
)
and
not
label
.
isAny
?
label
.
title
else
label
.
id
hidden
:
->
page
=
$
(
'body'
).
data
'page'
isIssueIndex
=
page
is
'projects:issues:index'
isMRIndex
=
page
is
'projects:merge_requests:index'
$selectbox
.
hide
()
# display:block overrides the hide-collapse rule
$value
.
removeAttr
(
'style'
)
if
$dropdown
.
hasClass
'js-multiselect'
saveLabelData
()
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
selectedLabels
=
$dropdown
.
closest
(
'form'
)
.
find
(
"input:hidden[name='
#{
$dropdown
.
data
(
'fieldName'
)
}
']"
)
Issuable
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
$dropdown
.
closest
(
'form'
).
submit
()
else
saveLabelData
()
multiSelect
:
$dropdown
.
hasClass
'js-multiselect'
clicked
:
(
label
)
->
page
=
$
(
'body'
).
data
'page'
isIssueIndex
=
page
is
'projects:issues:index'
isMRIndex
=
page
is
page
is
'projects:merge_requests:index'
isMRIndex
=
page
is
'projects:merge_requests:index'
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
selectedLabel
=
label
.
title
Issues
.
filterResults
$dropdown
.
closest
(
'form'
)
if
not
$dropdown
.
hasClass
'js-multiselect'
selectedLabel
=
label
.
title
Issuable
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
'js-filter-submit'
$dropdown
.
closest
(
'form'
).
submit
()
else
...
...
app/assets/javascripts/lib/animate.js.coffee
View file @
7e0ef892
((
w
)
->
if
not
w
.
gl
?
then
w
.
gl
=
{}
if
not
gl
.
animate
?
then
gl
.
animate
=
{}
w
.
glAnimate
=
(
$el
,
animation
,
done
)
->
gl
.
animate
.
animate
=
(
$el
,
animation
,
options
,
done
)
->
if
options
?
.
cssStart
?
$el
.
css
(
options
.
cssStart
)
$el
.
removeClass
()
.
removeClass
(
animation
+
' animated'
)
.
addClass
(
animation
+
' animated'
)
.
one
'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend'
,
->
$
(
this
).
removeClass
()
$
(
this
).
removeClass
(
animation
+
' animated'
)
if
done
?
done
()
if
options
?
.
cssEnd
?
$el
.
css
(
options
.
cssEnd
)
return
return
return
gl
.
animate
.
animateEach
=
(
$els
,
animation
,
time
,
options
,
done
)
->
dfd
=
$
.
Deferred
()
if
not
$els
.
length
dfd
.
resolve
()
$els
.
each
((
i
)
->
setTimeout
(
=>
$this
=
$
(
@
)
gl
.
animate
.
animate
(
$this
,
animation
,
options
,
=>
if
i
is
$els
.
length
-
1
dfd
.
resolve
()
if
done
?
done
()
)
,
time
*
i
)
return
)
return
dfd
.
promise
()
return
)
window
\ No newline at end of file
app/assets/javascripts/lib/url_utility.js.coffee
View file @
7e0ef892
...
...
@@ -3,16 +3,20 @@
w
.
gl
?=
{}
w
.
gl
.
utils
?=
{}
w
.
gl
.
utils
.
getUrlParameter
=
(
sParam
)
->
# Returns an array containing the value(s) of the
# of the key passed as an argument
w
.
gl
.
utils
.
getParameterValues
=
(
sParam
)
->
sPageURL
=
decodeURIComponent
(
window
.
location
.
search
.
substring
(
1
))
sURLVariables
=
sPageURL
.
split
(
'&'
)
sParameterName
=
undefined
values
=
[]
i
=
0
while
i
<
sURLVariables
.
length
sParameterName
=
sURLVariables
[
i
].
split
(
'='
)
if
sParameterName
[
0
]
is
sParam
return
if
sParameterName
[
1
]
is
undefined
then
true
else
sParameterName
[
1
]
values
.
push
(
sParameterName
[
1
])
i
++
values
# #
# @param {Object} params - url keys and value to merge
...
...
@@ -28,4 +32,12 @@
newUrl
=
"
#{
newUrl
}#{
(
if
newUrl
.
indexOf
(
'?'
)
>
0
then
'&'
else
'?'
)
}#{
paramName
}
=
#{
paramValue
}
"
newUrl
# removes parameter query string from url. returns the modified url
w
.
gl
.
utils
.
removeParamQueryString
=
(
url
,
param
)
->
url
=
decodeURIComponent
(
url
)
urlVariables
=
url
.
split
(
'&'
)
(
variables
for
variables
in
urlVariables
when
variables
.
indexOf
(
param
)
is
-
1
).
join
(
'&'
)
)
window
app/assets/javascripts/merge_requests.js.coffee
deleted
100644 → 0
View file @
a5290ac2
#
# * Filter merge requests
#
@
MergeRequests
=
init
:
->
MergeRequests
.
initSearch
()
# Make sure we trigger ajax request only after user stop typing
initSearch
:
->
@
timer
=
null
$
(
"#issue_search"
).
keyup
->
clearTimeout
(
@
timer
)
@
timer
=
setTimeout
(
MergeRequests
.
filterResults
,
500
)
filterResults
:
=>
form
=
$
(
"#issue_search_form"
)
search
=
$
(
"#issue_search"
).
val
()
$
(
'.merge-requests-holder'
).
css
(
"opacity"
,
'0.5'
)
issues_url
=
form
.
attr
(
'action'
)
+
'?'
+
form
.
serialize
()
$
.
ajax
type
:
"GET"
url
:
form
.
attr
(
'action'
)
data
:
form
.
serialize
()
complete
:
->
$
(
'.merge-requests-holder'
).
css
(
"opacity"
,
'1.0'
)
success
:
(
data
)
->
$
(
'.merge-requests-holder'
).
html
(
data
.
html
)
# Change url so if user reload a page - search results are saved
history
.
replaceState
{
page
:
issues_url
},
document
.
title
,
issues_url
MergeRequests
.
reload
()
dataType
:
"json"
reload
:
->
$
(
'#filter_issue_search'
).
val
(
$
(
'#issue_search'
).
val
())
app/assets/javascripts/milestone_select.js.coffee
View file @
7e0ef892
...
...
@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone
=
selected
.
name
else
selectedMilestone
=
''
Issu
es
.
filterResults
$dropdown
.
closest
(
'form'
)
Issu
able
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
$dropdown
.
closest
(
'form'
).
submit
()
else
...
...
app/assets/javascripts/raven_config.js.coffee
0 → 100644
View file @
7e0ef892
@
raven
=
init
:
->
if
gon
.
sentry_dsn
?
Raven
.
config
(
gon
.
sentry_dsn
,
{
includePaths
:
[
/gon.relative_url_root/
]
ignoreErrors
:
[
# Random plugins/extensions
'top.GLOBALS'
,
# See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
'originalCreateNotification'
,
'canvas.contentDocument'
,
'MyApp_RemoveAllHighlights'
,
'http://tt.epicplay.com'
,
'Can
\'
t find variable: ZiteReader'
,
'jigsaw is not defined'
,
'ComboSearch is not defined'
,
'http://loading.retry.widdit.com/'
,
'atomicFindClose'
,
# ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
# reduce this. (thanks @acdha)
# See http://stackoverflow.com/questions/4113268
'bmi_SafeAddOnload'
,
'EBCallBackMessageReceived'
,
# See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage'
],
ignoreUrls
:
[
# Chrome extensions
/extensions\//i
,
/^chrome:\/\//i
,
# Other plugins
/127\.0\.0\.1:4001\/isrunning/i
,
# Cacaoweb
/webappstoolbarba\.texthelp\.com\//i
,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i
]
}).
install
()
if
gon
.
current_user_id
Raven
.
setUserContext
({
id
:
gon
.
current_user_id
})
$
->
raven
.
init
()
app/assets/javascripts/users_select.js.coffee
View file @
7e0ef892
...
...
@@ -158,7 +158,7 @@ class @UsersSelect
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
selectedId
=
user
.
id
Issu
es
.
filterResults
$dropdown
.
closest
(
'form'
)
Issu
able
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
'js-filter-submit'
$dropdown
.
closest
(
'form'
).
submit
()
else
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
7e0ef892
...
...
@@ -248,7 +248,7 @@
.dropdown-title
{
position
:
relative
;
padding
:
0
25px
1
5
px
;
padding
:
0
25px
1
0
px
;
margin
:
0
10px
10px
;
font-weight
:
600
;
line-height
:
1
;
...
...
@@ -278,7 +278,7 @@
right
:
5px
;
width
:
20px
;
height
:
20px
;
top
:
-
1
px
;
top
:
-
3
px
;
}
.dropdown-menu-back
{
...
...
@@ -358,6 +358,13 @@
border-top
:
1px
solid
$dropdown-divider-color
;
}
.dropdown-due-date-footer
{
padding-top
:
0
;
margin-left
:
10px
;
margin-right
:
10px
;
border-top
:
0
;
}
.dropdown-footer-list
{
font-size
:
14px
;
...
...
@@ -395,3 +402,122 @@
height
:
15px
;
border-radius
:
$border-radius-base
;
}
.dropdown-menu-due-date
{
.dropdown-content
{
max-height
:
230px
;
}
.ui-widget
{
table
{
margin
:
0
;
}
&
.ui-datepicker-inline
{
padding
:
0
10px
;
border
:
0
;
width
:
100%
;
}
.ui-datepicker-header
{
padding
:
0
8px
10px
;
border
:
0
;
.ui-icon
{
background
:
none
;
font-size
:
20px
;
text-indent
:
0
;
&
:before
{
display
:
block
;
position
:
relative
;
top
:
-2px
;
color
:
$dropdown-title-btn-color
;
font
:
normal
normal
normal
14px
/
1
FontAwesome
;
font-size
:
inherit
;
text-rendering
:
auto
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
}
}
}
.ui-state-active
,
.ui-state-hover
{
color
:
$md-link-color
;
background-color
:
$calendar-hover-bg
;
}
.ui-datepicker-prev
,
.ui-datepicker-next
{
top
:
0
;
height
:
15px
;
cursor
:
pointer
;
&
:hover
{
background-color
:
transparent
;
border
:
0
;
.ui-icon
:before
{
color
:
$md-link-color
;
}
}
}
.ui-datepicker-prev
{
left
:
0
;
.ui-icon
:before
{
content
:
'\f104'
;
text-align
:
left
;
}
}
.ui-datepicker-next
{
right
:
0
;
.ui-icon
:before
{
content
:
'\f105'
;
text-align
:
right
;
}
}
td
{
padding
:
0
;
border
:
1px
solid
$calendar-border-color
;
&
:first-child
{
border-left
:
0
;
}
&
:last-child
{
border-right
:
0
;
}
a
{
line-height
:
17px
;
border
:
0
;
border-radius
:
0
;
}
}
.ui-datepicker-title
{
color
:
$gl-gray
;
font-size
:
15px
;
line-height
:
1
;
font-weight
:
normal
;
}
}
th
{
padding
:
2px
0
;
color
:
$calendar-header-color
;
font-weight
:
normal
;
text-transform
:
lowercase
;
border-top
:
1px
solid
$calendar-border-color
;
}
.ui-datepicker-unselectable
{
background-color
:
$calendar-unselectable-bg
;
}
}
app/assets/stylesheets/framework/markdown_area.scss
View file @
7e0ef892
...
...
@@ -90,3 +90,12 @@
box-shadow
:
none
;
width
:
100%
;
}
.md
{
&
.md-preview-holder
{
code
{
white-space
:
pre-wrap
;
word-break
:
break-all
;
}
}
}
app/assets/stylesheets/framework/variables.scss
View file @
7e0ef892
...
...
@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
$note-toolbar-color
:
#959494
;
$zen-control-hover-color
:
#111
;
$calendar-header-color
:
#b8b8b8
;
$calendar-hover-bg
:
#ecf3fe
;
$calendar-border-color
:
rgba
(
#000
,
.1
);
$calendar-unselectable-bg
:
#faf9f9
;
app/assets/stylesheets/pages/detail_page.scss
View file @
7e0ef892
...
...
@@ -36,4 +36,10 @@
}
}
}
.wiki
{
code
{
white-space
:
pre-wrap
;
}
}
}
app/assets/stylesheets/pages/diff.scss
View file @
7e0ef892
...
...
@@ -34,6 +34,7 @@
background
:
#fff
;
color
:
#333
;
border-radius
:
0
0
3px
3px
;
-webkit-overflow-scrolling
:
auto
;
.unfold
{
cursor
:
pointer
;
...
...
@@ -86,7 +87,7 @@
}
span
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
}
}
...
...
@@ -335,7 +336,7 @@
}
.diff-file
.line_content
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
.diff-wrap-lines
.line_content
{
...
...
app/assets/stylesheets/pages/help.scss
View file @
7e0ef892
...
...
@@ -59,8 +59,10 @@
position
:
relative
;
overflow-y
:
auto
;
padding
:
15px
;
.form-actions
{
margin
:
-
$gl-padding
+
1
;
margin-top
:
15px
;
}
}
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
7e0ef892
...
...
@@ -128,6 +128,7 @@
top
:
58px
;
bottom
:
0
;
right
:
0
;
z-index
:
10
;
transition
:
width
.3s
;
background
:
$gray-light
;
padding
:
10px
20px
;
...
...
@@ -241,7 +242,7 @@
}
}
.
btn
{
.
issuable-pager
{
background
:
$gray-normal
;
border
:
1px
solid
$border-gray-normal
;
&
:hover
{
...
...
@@ -254,7 +255,7 @@
}
}
a
:not
(
.
btn
)
{
a
:not
(
.
issuable-pager
)
{
&
:hover
{
color
:
$md-link-color
;
text-decoration
:
none
;
...
...
app/assets/stylesheets/pages/note_form.scss
View file @
7e0ef892
...
...
@@ -84,18 +84,6 @@
border-color
:
$gl-success
;
}
}
p
{
code
{
white-space
:
normal
;
}
pre
{
code
{
white-space
:
pre
;
}
}
}
}
}
...
...
app/assets/stylesheets/pages/notes.scss
View file @
7e0ef892
...
...
@@ -81,16 +81,8 @@ ul.notes {
@include
md-typography
;
// On diffs code should wrap nicely and not overflow
p
{
code
{
white-space
:
normal
;
}
pre
{
code
{
white-space
:
pre
;
}
}
code
{
white-space
:
pre-wrap
;
}
// Reset ul style types since we're nested inside a ul already
...
...
@@ -137,7 +129,7 @@ ul.notes {
margin-right
:
10px
;
}
.line_content
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
}
...
...
@@ -171,11 +163,6 @@ ul.notes {
&
.parallel
{
border-width
:
1px
;
.code
,
code
{
white-space
:
pre-wrap
;
}
}
.notes
{
...
...
@@ -308,7 +295,7 @@ ul.notes {
padding
:
4px
;
font-size
:
16px
;
color
:
$gl-link-color
;
margin-left
:
-
60
px
;
margin-left
:
-
56
px
;
position
:
absolute
;
z-index
:
10
;
width
:
32px
;
...
...
app/controllers/projects/issues_controller.rb
View file @
7e0ef892
...
...
@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues
=
@issues
.
page
(
params
[
:page
])
@label
=
@project
.
labels
.
find_by
(
title:
params
[
:label_name
])
@label
s
=
@project
.
labels
.
where
(
title:
params
[
:label_name
])
respond_to
do
|
format
|
format
.
html
format
.
atom
{
render
layout:
false
}
format
.
json
do
render
json:
{
html:
view_to_html_string
(
"projects/issues/_issues"
)
html:
view_to_html_string
(
"projects/issues/_issues"
),
labels:
@labels
.
as_json
(
methods: :text_color
)
}
end
end
...
...
@@ -191,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def
issue_params
params
.
require
(
:issue
).
permit
(
:title
,
:assignee_id
,
:position
,
:description
,
:confidential
,
:milestone_id
,
:state_event
,
:task_num
,
label_ids:
[]
:milestone_id
,
:
due_date
,
:
state_event
,
:task_num
,
label_ids:
[]
)
end
...
...
app/controllers/projects/merge_requests_controller.rb
View file @
7e0ef892
...
...
@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests
=
@merge_requests
.
page
(
params
[
:page
])
@merge_requests
=
@merge_requests
.
preload
(
:target_project
)
@label
=
@project
.
labels
.
find_by
(
title:
params
[
:label_name
])
@label
s
=
@project
.
labels
.
where
(
title:
params
[
:label_name
])
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
{
html:
view_to_html_string
(
"projects/merge_requests/_merge_requests"
)
html:
view_to_html_string
(
"projects/merge_requests/_merge_requests"
),
labels:
@labels
.
as_json
(
methods: :text_color
)
}
end
end
...
...
app/finders/issuable_finder.rb
View file @
7e0ef892
...
...
@@ -39,6 +39,7 @@ class IssuableFinder
items
=
by_assignee
(
items
)
items
=
by_author
(
items
)
items
=
by_label
(
items
)
items
=
by_due_date
(
items
)
sort
(
items
)
end
...
...
@@ -117,7 +118,7 @@ class IssuableFinder
end
def
filter_by_no_label?
labels?
&&
params
[
:label_name
]
==
Label
::
None
.
title
labels?
&&
params
[
:label_name
]
.
include?
(
Label
::
None
.
title
)
end
def
labels
...
...
@@ -278,9 +279,47 @@ class IssuableFinder
end
end
# When filtering by multiple labels we may end up duplicating issues (if one
# has multiple labels). This ensures we only return unique issues.
items
.
distinct
end
def
by_due_date
(
items
)
if
due_date?
if
filter_by_no_due_date?
items
=
items
.
without_due_date
elsif
filter_by_overdue?
items
=
items
.
due_before
(
Date
.
today
)
elsif
filter_by_due_this_week?
items
=
items
.
due_between
(
Date
.
today
.
beginning_of_week
,
Date
.
today
.
end_of_week
)
elsif
filter_by_due_this_month?
items
=
items
.
due_between
(
Date
.
today
.
beginning_of_month
,
Date
.
today
.
end_of_month
)
end
end
items
end
def
filter_by_no_due_date?
due_date?
&&
params
[
:due_date
]
==
Issue
::
NoDueDate
.
name
end
def
filter_by_overdue?
due_date?
&&
params
[
:due_date
]
==
Issue
::
Overdue
.
name
end
def
filter_by_due_this_week?
due_date?
&&
params
[
:due_date
]
==
Issue
::
DueThisWeek
.
name
end
def
filter_by_due_this_month?
due_date?
&&
params
[
:due_date
]
==
Issue
::
DueThisMonth
.
name
end
def
due_date?
params
[
:due_date
].
present?
&&
klass
.
column_names
.
include?
(
'due_date'
)
end
def
label_names
params
[
:label_name
].
split
(
','
)
end
...
...
app/helpers/application_helper.rb
View file @
7e0ef892
...
...
@@ -254,11 +254,11 @@ module ApplicationHelper
def
page_filter_path
(
options
=
{})
without
=
options
.
delete
(
:without
)
add_label
=
options
.
delete
(
:label
)
exist_opts
=
{
state:
params
[
:state
],
scope:
params
[
:scope
],
label_name:
params
[
:label_name
],
milestone_title:
params
[
:milestone_title
],
assignee_id:
params
[
:assignee_id
],
author_id:
params
[
:author_id
],
...
...
@@ -275,6 +275,13 @@ module ApplicationHelper
path
=
request
.
path
path
<<
"?
#{
options
.
to_param
}
"
if
add_label
if
params
[
:label_name
].
present?
and
params
[
:label_name
].
respond_to?
(
'any?'
)
params
[
:label_name
].
each
do
|
label
|
path
<<
"&label_name[]=
#{
label
}
"
end
end
end
path
end
...
...
app/helpers/issuables_helper.rb
View file @
7e0ef892
...
...
@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope
(
issuable
).
where
(
'iid > ?'
,
issuable
.
iid
).
last
end
def
multi_label_name
(
current_labels
,
default_label
)
# current_labels may be a string from before
if
current_labels
.
is_a?
(
Array
)
if
current_labels
.
count
>
1
"
#{
current_labels
[
0
]
}
+
#{
current_labels
.
count
-
1
}
more"
else
current_labels
[
0
]
end
elsif
current_labels
.
is_a?
(
String
)
if
current_labels
.
nil?
||
current_labels
.
empty?
default_label
else
current_labels
end
else
default_label
end
end
def
issuable_json_path
(
issuable
)
project
=
issuable
.
project
...
...
app/helpers/issues_helper.rb
View file @
7e0ef892
...
...
@@ -131,7 +131,7 @@ module IssuesHelper
class:
"icon emoji-icon emoji-
#{
unicode
}
"
,
title:
name
,
data:
data
else
else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag
:img
,
""
,
...
...
@@ -172,6 +172,18 @@ module IssuesHelper
end
.
to_h
end
def
due_date_options
options
=
[
Issue
::
AnyDueDate
,
Issue
::
NoDueDate
,
Issue
::
DueThisWeek
,
Issue
::
DueThisMonth
,
Issue
::
Overdue
]
options_from_collection_for_select
(
options
,
'name'
,
'title'
,
params
[
:due_date
])
end
# Required for Banzai::Filter::IssueReferenceFilter
module_function
:url_for_issue
end
app/helpers/sorting_helper.rb
View file @
7e0ef892
...
...
@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created
=>
sort_title_oldest_created
,
sort_value_milestone_soon
=>
sort_title_milestone_soon
,
sort_value_milestone_later
=>
sort_title_milestone_later
,
sort_value_due_date_soon
=>
sort_title_due_date_soon
,
sort_value_due_date_later
=>
sort_title_due_date_later
,
sort_value_largest_repo
=>
sort_title_largest_repo
,
sort_value_recently_signin
=>
sort_title_recently_signin
,
sort_value_oldest_signin
=>
sort_title_oldest_signin
,
...
...
@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later'
end
def
sort_title_due_date_soon
'Due soon'
end
def
sort_title_due_date_later
'Due later'
end
def
sort_title_name
'Name'
end
...
...
@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc'
end
def
sort_value_due_date_soon
'due_date_asc'
end
def
sort_value_due_date_later
'due_date_desc'
end
def
sort_value_name
'name_asc'
end
...
...
app/models/issue.rb
View file @
7e0ef892
...
...
@@ -28,6 +28,13 @@ class Issue < ActiveRecord::Base
include
Sortable
include
Taskable
DueDateStruct
=
Struct
.
new
(
:title
,
:name
).
freeze
NoDueDate
=
DueDateStruct
.
new
(
'No Due Date'
,
'0'
).
freeze
AnyDueDate
=
DueDateStruct
.
new
(
'Any Due Date'
,
''
).
freeze
Overdue
=
DueDateStruct
.
new
(
'Overdue'
,
'overdue'
).
freeze
DueThisWeek
=
DueDateStruct
.
new
(
'Due This Week'
,
'week'
).
freeze
DueThisMonth
=
DueDateStruct
.
new
(
'Due This Month'
,
'month'
).
freeze
ActsAsTaggableOn
.
strict_case_match
=
true
belongs_to
:project
...
...
@@ -39,6 +46,13 @@ class Issue < ActiveRecord::Base
scope
:open_for
,
->
(
user
)
{
opened
.
assigned_to
(
user
)
}
scope
:in_projects
,
->
(
project_ids
)
{
where
(
project_id:
project_ids
)
}
scope
:without_due_date
,
->
{
where
(
due_date:
nil
)
}
scope
:due_before
,
->
(
date
)
{
where
(
'issues.due_date < ?'
,
date
)
}
scope
:due_between
,
->
(
from_date
,
to_date
)
{
where
(
'issues.due_date >= ?'
,
from_date
).
where
(
'issues.due_date <= ?'
,
to_date
)
}
scope
:order_due_date_asc
,
->
{
reorder
(
'issues.due_date IS NULL, issues.due_date ASC'
)
}
scope
:order_due_date_desc
,
->
{
reorder
(
'issues.due_date IS NULL, issues.due_date DESC'
)
}
state_machine
:state
,
initial: :opened
do
event
:close
do
transition
[
:reopened
,
:opened
]
=>
:closed
...
...
@@ -82,6 +96,15 @@ class Issue < ActiveRecord::Base
@link_reference_pattern
||=
super
(
"issues"
,
/(?<issue>\d+)/
)
end
def
self
.
sort
(
method
)
case
method
.
to_s
when
'due_date_asc'
then
order_due_date_asc
when
'due_date_desc'
then
order_due_date_desc
else
super
end
end
def
to_reference
(
from_project
=
nil
)
reference
=
"
#{
self
.
class
.
reference_prefix
}#{
iid
}
"
...
...
@@ -169,4 +192,8 @@ class Issue < ActiveRecord::Base
self
.
related_branches
(
current_user
).
empty?
&&
self
.
closed_by_merge_requests
(
current_user
).
empty?
end
def
overdue?
due_date
.
try
(
:past?
)
||
false
end
end
app/models/label.rb
View file @
7e0ef892
...
...
@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template
end
def
text_color
LabelsHelper
::
text_color_for_bg
(
self
.
color
)
end
private
def
label_format_reference
(
format
=
:id
)
...
...
app/views/projects/_activity.html.haml
View file @
7e0ef892
...
...
@@ -9,4 +9,7 @@
=
spinner
:javascript
new
Activities
();
var
activity
=
new
Activities
();
$
(
document
).
on
(
'
page:restore
'
,
function
(
event
)
{
activity
.
reloadActivities
()
})
app/views/projects/issues/_issue.html.haml
View file @
7e0ef892
...
...
@@ -48,6 +48,11 @@
=
link_to
namespace_project_issues_path
(
issue
.
project
.
namespace
,
issue
.
project
,
milestone_title:
issue
.
milestone
.
title
)
do
=
icon
(
'clock-o'
)
=
issue
.
milestone
.
title
-
if
issue
.
due_date
%span
{
class:
"#{'cred' if issue.overdue?}"
}
=
icon
(
'calendar'
)
=
issue
.
due_date
.
to_s
(
:medium
)
-
if
issue
.
labels
.
any?
-
issue
.
labels
.
each
do
|
label
|
...
...
app/views/projects/merge_requests/show/_mr_title.html.haml
View file @
7e0ef892
...
...
@@ -24,9 +24,9 @@
%li
{
class:
issue_button_visibility
(
@merge_request
,
false
)
}
=
link_to
'Reopen'
,
merge_request_path
(
@merge_request
,
merge_request:
{
state_event: :reopen
}),
method: :put
,
class:
'reopen-mr-link'
,
title:
'Reopen merge request'
%li
=
link_to
'Edit'
,
edit_namespace_project_merge_request_path
(
@project
.
namespace
,
@project
,
@merge_request
),
class:
'issuable-edit'
,
id:
'edit_merge_request'
=
link_to
'Edit'
,
edit_namespace_project_merge_request_path
(
@project
.
namespace
,
@project
,
@merge_request
),
class:
'issuable-edit'
=
link_to
'Close'
,
merge_request_path
(
@merge_request
,
merge_request:
{
state_event: :close
}),
method: :put
,
class:
"hidden-xs hidden-sm btn btn-nr btn-grouped btn-close
#{
issue_button_visibility
(
@merge_request
,
true
)
}
"
,
title:
'Close merge request'
=
link_to
'Reopen'
,
merge_request_path
(
@merge_request
,
merge_request:
{
state_event: :reopen
}),
method: :put
,
class:
"hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link
#{
issue_button_visibility
(
@merge_request
,
false
)
}
"
,
title:
'Reopen merge request'
=
link_to
edit_namespace_project_merge_request_path
(
@project
.
namespace
,
@project
,
@merge_request
),
class:
"hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit"
,
id:
'edit_merge_request'
do
=
link_to
edit_namespace_project_merge_request_path
(
@project
.
namespace
,
@project
,
@merge_request
),
class:
"hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit"
do
=
icon
(
'pencil-square-o'
)
Edit
app/views/shared/_label_row.html.haml
View file @
7e0ef892
...
...
@@ -2,4 +2,4 @@
%span
.label-name
=
link_to_label
(
label
,
tooltip:
false
)
%span
.prepend-left-10
=
markdown
(
label
.
description
,
pipeline: :single_line
)
=
markdown
(
label
.
description
,
pipeline: :single_line
)
\ No newline at end of file
app/views/shared/_labels_row.html.haml
0 → 100644
View file @
7e0ef892
-
labels
.
each
do
|
label
|
%span
.label-row
=
link_to_label
(
label
,
tooltip:
false
)
app/views/shared/_sort_dropdown.html.haml
View file @
7e0ef892
...
...
@@ -20,6 +20,11 @@
=
sort_title_milestone_soon
=
link_to
page_filter_path
(
sort:
sort_value_milestone_later
)
do
=
sort_title_milestone_later
-
if
controller
.
controller_name
==
'issues'
||
controller
.
action_name
==
'issues'
=
link_to
page_filter_path
(
sort:
sort_value_due_date_soon
)
do
=
sort_title_due_date_soon
=
link_to
page_filter_path
(
sort:
sort_value_due_date_later
)
do
=
sort_title_due_date_later
=
link_to
page_filter_path
(
sort:
sort_value_upvotes
)
do
=
sort_title_upvotes
=
link_to
page_filter_path
(
sort:
sort_value_downvotes
)
do
...
...
app/views/shared/issuable/_filter.html.haml
View file @
7e0ef892
...
...
@@ -23,6 +23,7 @@
.filter-item.inline.labels-filter
=
render
"shared/issuable/label_dropdown"
.pull-right
=
render
'shared/sort_dropdown'
...
...
@@ -46,9 +47,10 @@
.filter-item.inline
=
button_tag
"Update issues"
,
class:
"btn update_selected_issues btn-save"
-
if
@label
.gray-content-block.second-block
=
render
"shared/label_row"
,
label:
@label
-
if
!
@labels
.
nil?
.gray-content-block.second-block.filtered-labels
{
class:
(
"hidden"
if
!
@labels
.
any?
)
}
-
if
@labels
.
any?
=
render
"shared/labels_row"
,
labels:
@labels
:javascript
new
UsersSelect
();
...
...
app/views/shared/issuable/_label_dropdown.html.haml
View file @
7e0ef892
-
if
params
[
:label_name
].
present?
=
hidden_field_tag
(
:label_name
,
params
[
:label_name
])
-
if
params
[
:label_name
].
respond_to?
(
'any?'
)
-
params
[
:label_name
].
each
do
|
label
|
=
hidden_field_tag
"label_name[]"
,
label
,
id:
nil
.dropdown
%button
.dropdown-menu-toggle.js-label-select.js-filter-submit.js-
extra-options
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
field_name:
"label_name
"
,
show_no:
"true"
,
show_any:
"true"
,
selected:
params
[
:label_name
],
project_id:
@project
.
try
(
:id
),
labels:
labels_filter_path
,
default_label:
"Label"
}}
%button
.dropdown-menu-toggle.js-label-select.js-filter-submit.js-
multiselect.js-extra-options
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
field_name:
"label_name[]
"
,
show_no:
"true"
,
show_any:
"true"
,
selected:
params
[
:label_name
],
project_id:
@project
.
try
(
:id
),
labels:
labels_filter_path
,
default_label:
"Label"
}}
%span
.dropdown-toggle-text
=
h
(
params
[
:label_name
].
presence
||
"Label"
)
=
h
(
multi_label_name
(
params
[
:label_name
],
"Label"
)
)
=
icon
(
'chevron-down'
)
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
title:
"Filter by label"
}
...
...
app/views/shared/issuable/_nav.html.haml
View file @
7e0ef892
...
...
@@ -4,22 +4,22 @@
-
else
-
page_context_word
=
'issues'
%li
{
class:
(
"active"
if
params
[
:state
]
==
'opened'
)}
=
link_to
page_filter_path
(
state:
'opened'
),
title:
"Filter by
#{
page_context_word
}
that are currently opened."
do
=
link_to
page_filter_path
(
state:
'opened'
,
label:
true
),
title:
"Filter by
#{
page_context_word
}
that are currently opened."
do
#{
state_filters_text_for
(
:opened
,
@project
)
}
-
if
defined?
(
type
)
&&
type
==
:merge_requests
%li
{
class:
(
"active"
if
params
[
:state
]
==
'merged'
)}
=
link_to
page_filter_path
(
state:
'merged'
),
title:
'Filter by merge requests that are currently merged.'
do
=
link_to
page_filter_path
(
state:
'merged'
,
label:
true
),
title:
'Filter by merge requests that are currently merged.'
do
#{
state_filters_text_for
(
:merged
,
@project
)
}
%li
{
class:
(
"active"
if
params
[
:state
]
==
'closed'
)}
=
link_to
page_filter_path
(
state:
'closed'
),
title:
'Filter by merge requests that are currently closed and unmerged.'
do
=
link_to
page_filter_path
(
state:
'closed'
,
label:
true
),
title:
'Filter by merge requests that are currently closed and unmerged.'
do
#{
state_filters_text_for
(
:closed
,
@project
)
}
-
else
%li
{
class:
(
"active"
if
params
[
:state
]
==
'closed'
)}
=
link_to
page_filter_path
(
state:
'closed'
),
title:
'Filter by issues that are currently closed.'
do
=
link_to
page_filter_path
(
state:
'closed'
,
label:
true
),
title:
'Filter by issues that are currently closed.'
do
#{
state_filters_text_for
(
:closed
,
@project
)
}
%li
{
class:
(
"active"
if
params
[
:state
]
==
'all'
)}
=
link_to
page_filter_path
(
state:
'all'
),
title:
"Show all
#{
page_context_word
}
."
do
=
link_to
page_filter_path
(
state:
'all'
,
label:
true
),
title:
"Show all
#{
page_context_word
}
."
do
#{
state_filters_text_for
(
:all
,
@project
)
}
app/views/shared/issuable/_sidebar.html.haml
View file @
7e0ef892
...
...
@@ -10,14 +10,14 @@
=
sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group
{
role:
'group'
,
"aria-label"
=>
'...'
}
-
if
prev_issuable
=
prev_issuable_for
(
issuable
)
=
link_to
'Prev'
,
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
prev_issuable
],
class:
'btn btn-default prev-btn'
=
link_to
'Prev'
,
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
prev_issuable
],
class:
'btn btn-default prev-btn
issuable-pager
'
-
else
%a
.btn.btn-default.disabled
{
href:
'#'
}
%a
.btn.btn-default.
issuable-pager.
disabled
{
href:
'#'
}
Prev
-
if
next_issuable
=
next_issuable_for
(
issuable
)
=
link_to
'Next'
,
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
next_issuable
],
class:
'btn btn-default next-btn'
=
link_to
'Next'
,
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
next_issuable
],
class:
'btn btn-default next-btn
issuable-pager
'
-
else
%a
.btn.btn-default.disabled
{
href:
'#'
}
%a
.btn.btn-default.
issuable-pager.
disabled
{
href:
'#'
}
Next
=
form_for
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
issuable
],
remote:
true
,
html:
{
class:
'issuable-context-form inline-update js-issuable-update'
}
do
|
f
|
...
...
@@ -58,7 +58,7 @@
-
if
issuable
.
milestone
=
issuable
.
milestone
.
title
-
else
No
No
ne
.title.hide-collapsed
Milestone
=
icon
(
'spinner spin'
,
class:
'block-loading'
)
...
...
@@ -75,6 +75,34 @@
=
f
.
hidden_field
'milestone_id'
,
value:
issuable
.
milestone_id
,
id:
nil
=
dropdown_tag
(
'Milestone'
,
options:
{
title:
'Assign milestone'
,
toggle_class:
'js-milestone-select js-extra-options'
,
filter:
true
,
dropdown_class:
'dropdown-menu-selectable'
,
placeholder:
'Search milestones'
,
data:
{
show_no:
true
,
field_name:
"
#{
issuable
.
to_ability_name
}
[milestone_id]"
,
project_id:
@project
.
id
,
issuable_id:
issuable
.
id
,
milestones:
namespace_project_milestones_path
(
@project
.
namespace
,
@project
,
:json
),
ability_name:
issuable
.
to_ability_name
,
issue_update:
issuable_json_path
(
issuable
),
use_id:
true
}})
-
if
issuable
.
has_attribute?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon
=
icon
(
'calendar'
)
%span
.js-due-date-sidebar-value
=
issuable
.
due_date
.
try
(
:to_s
,
:medium
)
||
'None'
.title.hide-collapsed
Due date
=
icon
(
'spinner spin'
,
class:
'block-loading'
)
-
if
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
.value.bold.hide-collapsed
-
if
issuable
.
due_date
=
issuable
.
due_date
.
to_s
(
:medium
)
-
else
.light
None
-
if
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
.selectbox.hide-collapsed
=
f
.
hidden_field
:due_date
,
value:
issuable
.
due_date
.dropdown
%button
.dropdown-menu-toggle.js-due-date-select
{
type:
'button'
,
data:
{
toggle:
'dropdown'
,
field_name:
"#{issuable.to_ability_name}[due_date]"
,
ability_name:
issuable
.
to_ability_name
,
issue_update:
issuable_json_path
(
issuable
)
}
}
%span
.dropdown-toggle-text
Due date
=
icon
(
'chevron-down'
)
.dropdown-menu.dropdown-menu-due-date
=
dropdown_title
(
'Due date'
)
=
dropdown_content
do
.js-due-date-calendar
-
if
issuable
.
project
.
labels
.
any?
.block.labels
.sidebar-collapsed-icon
...
...
@@ -139,3 +167,4 @@
new
IssuableContext
(
'
#{
escape_javascript
(
current_user
.
to_json
(
only:
[
:username
,
:id
,
:name
]))
}
'
);
new
Subscription
(
'
.subscription
'
)
new
Sidebar
();
new
DueDateSelect
();
db/migrate/20160310124959_add_due_date_to_issues.rb
0 → 100644
View file @
7e0ef892
class
AddDueDateToIssues
<
ActiveRecord
::
Migration
def
change
add_column
:issues
,
:due_date
,
:date
add_index
:issues
,
:due_date
end
end
db/schema.rb
View file @
7e0ef892
...
...
@@ -422,6 +422,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
t
.
integer
"moved_to_id"
t
.
boolean
"confidential"
,
default:
false
t
.
datetime
"deleted_at"
t
.
date
"due_date"
end
add_index
"issues"
,
[
"assignee_id"
],
name:
"index_issues_on_assignee_id"
,
using: :btree
...
...
@@ -431,6 +432,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
add_index
"issues"
,
[
"created_at"
],
name:
"index_issues_on_created_at"
,
using: :btree
add_index
"issues"
,
[
"deleted_at"
],
name:
"index_issues_on_deleted_at"
,
using: :btree
add_index
"issues"
,
[
"description"
],
name:
"index_issues_on_description_trigram"
,
using: :gin
,
opclasses:
{
"description"
=>
"gin_trgm_ops"
}
add_index
"issues"
,
[
"due_date"
],
name:
"index_issues_on_due_date"
,
using: :btree
add_index
"issues"
,
[
"milestone_id"
],
name:
"index_issues_on_milestone_id"
,
using: :btree
add_index
"issues"
,
[
"project_id"
,
"iid"
],
name:
"index_issues_on_project_id_and_iid"
,
unique:
true
,
using: :btree
add_index
"issues"
,
[
"project_id"
],
name:
"index_issues_on_project_id"
,
using: :btree
...
...
features/project/issues/filter_labels.feature
View file @
7e0ef892
...
...
@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
@javascript
Scenario
:
I
filter by one label
Given
I click link
"bug"
And
I click
"dropdown close button"
Then
I should see
"Bugfix1"
in issues list
And
I should see
"Bugfix2"
in issues list
And
I should not see
"Feature1"
in issues list
...
...
features/steps/project/issues/filter_labels.rb
View file @
7e0ef892
...
...
@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
end
step
'I click "dropdown close button"'
do
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
...
...
lib/api/internal.rb
View file @
7e0ef892
...
...
@@ -23,7 +23,7 @@ module API
end
post
"/allowed"
do
Gitlab
::
Metrics
.
tag_transaction
(
'action'
,
'Grape#/internal/allowed'
)
Gitlab
::
Metrics
.
action
=
'Grape#/internal/allowed'
status
200
...
...
lib/gitlab/gon_helper.rb
View file @
7e0ef892
...
...
@@ -7,6 +7,7 @@ module Gitlab
gon
.
max_file_size
=
current_application_settings
.
max_attachment_size
gon
.
relative_url_root
=
Gitlab
.
config
.
gitlab
.
relative_url_root
gon
.
user_color_scheme
=
Gitlab
::
ColorSchemes
.
for_user
(
current_user
).
css_class
gon
.
sentry_dsn
=
ApplicationSetting
.
current
.
sentry_dsn
if
Rails
.
env
.
production?
if
current_user
gon
.
current_user_id
=
current_user
.
id
...
...
lib/gitlab/metrics.rb
View file @
7e0ef892
...
...
@@ -115,6 +115,15 @@ module Gitlab
trans
.
add_tag
(
name
,
value
)
if
trans
end
# Sets the action of the current transaction (if any)
#
# action - The name of the action.
def
self
.
action
=
(
action
)
trans
=
current_transaction
trans
.
action
=
action
if
trans
end
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if
enabled?
...
...
spec/features/issues/filter_by_labels_spec.rb
0 → 100644
View file @
7e0ef892
require
'rails_helper'
feature
'Issue filtering by Labels'
,
feature:
true
do
let
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)}
let!
(
:label
)
{
create
(
:label
,
project:
project
)
}
before
do
[
'bug'
,
'feature'
,
'enhancement'
].
each
do
|
title
|
create
(
:label
,
project:
project
,
title:
title
)
end
issue1
=
create
(
:issue
,
title:
"Bugfix1"
,
project:
project
)
issue1
.
labels
<<
project
.
labels
.
find_by
(
title:
'bug'
)
issue2
=
create
(
:issue
,
title:
"Bugfix2"
,
project:
project
)
issue2
.
labels
<<
project
.
labels
.
find_by
(
title:
'bug'
)
issue2
.
labels
<<
project
.
labels
.
find_by
(
title:
'enhancement'
)
issue3
=
create
(
:issue
,
title:
"Feature1"
,
project:
project
)
issue3
.
labels
<<
project
.
labels
.
find_by
(
title:
'feature'
)
project
.
team
<<
[
user
,
:master
]
login_as
(
user
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
)
end
context
'filter by label bug'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix1" and "Bugfix2" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix1"
expect
(
page
).
to
have_content
"Bugfix2"
end
it
'should not show "Feature1" in issues list'
do
expect
(
page
).
not_to
have_content
"Feature1"
end
it
'should show label "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"bug"
end
it
'should not show label "feature" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"enhancement"
end
end
context
'filter by label feature'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
feature
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Feature1" in issues list'
do
expect
(
page
).
to
have_content
"Feature1"
end
it
'should not show "Bugfix1" and "Bugfix2" in issues list'
do
expect
(
page
).
not_to
have_content
"Bugfix2"
expect
(
page
).
not_to
have_content
"Bugfix1"
end
it
'should show label "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"feature"
end
it
'should not show label "bug" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"enhancement"
end
end
context
'filter by label enhancement'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix2" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix2"
end
it
'should not show "Feature1" and "Bugfix1" in issues list'
do
expect
(
page
).
not_to
have_content
"Feature1"
expect
(
page
).
not_to
have_content
"Bugfix1"
end
it
'should show label "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
end
it
'should not show label "feature" and "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
end
end
context
'filter by label enhancement or feature'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
feature
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix2" or "Feature1" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix2"
expect
(
page
).
to
have_content
"Feature1"
end
it
'should not show "Bugfix1" in issues list'
do
expect
(
page
).
not_to
have_content
"Bugfix1"
end
it
'should show label "enhancement" and "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"feature"
end
it
'should not show label "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
end
end
context
'filter by label enhancement or bug in issues list'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix2" or "Bugfix1" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix2"
expect
(
page
).
to
have_content
"Bugfix1"
end
it
'should not show "Feature1"'
do
expect
(
page
).
not_to
have_content
"Feature1"
end
it
'should show label "bug" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
end
it
'should not show label "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
end
end
end
spec/features/issues/filter_issues_spec.rb
View file @
7e0ef892
...
...
@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
it
'should filter by any label'
do
find
(
'.dropdown-menu-labels a'
,
text:
'Any Label'
).
click
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
page
.
within
'.labels-filter'
do
expect
(
page
).
to
have_content
'Any Label'
end
expect
(
find
(
'.js-label-select .dropdown-toggle-text'
)).
to
have_content
(
'Label'
)
expect
(
find
(
'.js-label-select .dropdown-toggle-text'
)).
to
have_content
(
'
Any
Label'
)
end
it
'should filter by no label'
do
find
(
'.dropdown-menu-labels a'
,
text:
'No Label'
).
click
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
page
.
within
'.labels-filter'
do
expect
(
page
).
to
have_content
'No Label'
end
...
...
@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
find
(
'.js-label-select'
).
click
find
(
'.dropdown-menu-labels .dropdown-content a'
,
text:
label
.
title
).
click
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
...
...
spec/features/issues_spec.rb
View file @
7e0ef892
...
...
@@ -112,7 +112,7 @@ describe 'Issues', feature: true do
end
describe
'filter issue'
do
titles
=
[
'foo'
,
'bar'
,
'baz'
]
titles
=
%w[foo bar baz
]
titles
.
each_with_index
do
|
title
,
index
|
let!
(
title
.
to_sym
)
do
create
(
:issue
,
title:
title
,
...
...
@@ -153,8 +153,94 @@ describe 'Issues', feature: true do
expect
(
first_issue
).
to
include
(
'baz'
)
end
describe
'sorting by due date'
do
before
do
foo
.
update
(
due_date:
1
.
day
.
from_now
)
bar
.
update
(
due_date:
6
.
days
.
from_now
)
end
it
'sorts by recently due date'
do
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
sort:
sort_value_due_date_soon
)
expect
(
first_issue
).
to
include
(
'foo'
)
end
it
'sorts by least recently due date'
do
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
sort:
sort_value_due_date_later
)
expect
(
first_issue
).
to
include
(
'bar'
)
end
it
'sorts by least recently due date by excluding nil due dates'
do
bar
.
update
(
due_date:
nil
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
sort:
sort_value_due_date_later
)
expect
(
first_issue
).
to
include
(
'foo'
)
end
end
describe
'filtering by due date'
do
before
do
foo
.
update
(
due_date:
1
.
day
.
from_now
)
bar
.
update
(
due_date:
6
.
days
.
from_now
)
end
it
'filters by none'
do
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
due_date:
Issue
::
NoDueDate
.
name
)
expect
(
page
).
not_to
have_content
(
'foo'
)
expect
(
page
).
not_to
have_content
(
'bar'
)
expect
(
page
).
to
have_content
(
'baz'
)
end
it
'filters by any'
do
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
due_date:
Issue
::
AnyDueDate
.
name
)
expect
(
page
).
to
have_content
(
'foo'
)
expect
(
page
).
to
have_content
(
'bar'
)
expect
(
page
).
to
have_content
(
'baz'
)
end
it
'filters by due this week'
do
foo
.
update
(
due_date:
Date
.
today
.
beginning_of_week
+
2
.
days
)
bar
.
update
(
due_date:
Date
.
today
.
end_of_week
)
baz
.
update
(
due_date:
Date
.
today
-
8
.
days
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
due_date:
Issue
::
DueThisWeek
.
name
)
expect
(
page
).
to
have_content
(
'foo'
)
expect
(
page
).
to
have_content
(
'bar'
)
expect
(
page
).
not_to
have_content
(
'baz'
)
end
it
'filters by due this month'
do
foo
.
update
(
due_date:
Date
.
today
.
beginning_of_month
+
2
.
days
)
bar
.
update
(
due_date:
Date
.
today
.
end_of_month
)
baz
.
update
(
due_date:
Date
.
today
-
50
.
days
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
due_date:
Issue
::
DueThisMonth
.
name
)
expect
(
page
).
to
have_content
(
'foo'
)
expect
(
page
).
to
have_content
(
'bar'
)
expect
(
page
).
not_to
have_content
(
'baz'
)
end
it
'filters by overdue'
do
foo
.
update
(
due_date:
Date
.
today
+
2
.
days
)
bar
.
update
(
due_date:
Date
.
today
+
20
.
days
)
baz
.
update
(
due_date:
Date
.
yesterday
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
,
due_date:
Issue
::
Overdue
.
name
)
expect
(
page
).
not_to
have_content
(
'foo'
)
expect
(
page
).
not_to
have_content
(
'bar'
)
expect
(
page
).
to
have_content
(
'baz'
)
end
end
describe
'sorting by milestone'
do
before
:each
do
before
do
foo
.
milestone
=
newer_due_milestone
foo
.
save
bar
.
milestone
=
later_due_milestone
...
...
@@ -177,7 +263,7 @@ describe 'Issues', feature: true do
describe
'combine filter and sort'
do
let
(
:user2
)
{
create
(
:user
)
}
before
:each
do
before
do
foo
.
assignee
=
user2
foo
.
save
bar
.
assignee
=
user2
...
...
@@ -224,7 +310,7 @@ describe 'Issues', feature: true do
let
(
:guest
)
{
create
(
:user
)
}
before
:each
do
before
do
project
.
team
<<
[[
guest
],
:guest
]
end
...
...
@@ -267,7 +353,7 @@ describe 'Issues', feature: true do
context
'by unauthorized user'
do
let
(
:guest
)
{
create
(
:user
)
}
before
:each
do
before
do
project
.
team
<<
[
guest
,
:guest
]
issue
.
milestone
=
milestone
issue
.
save
...
...
@@ -285,7 +371,7 @@ describe 'Issues', feature: true do
describe
'removing assignee'
do
let
(
:user2
)
{
create
(
:user
)
}
before
:each
do
before
do
issue
.
assignee
=
user2
issue
.
save
end
...
...
spec/features/merge_requests/filter_by_milestone_spec.rb
View file @
7e0ef892
...
...
@@ -2,8 +2,14 @@ require 'rails_helper'
feature
'Merge Request filtering by Milestone'
,
feature:
true
do
let
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)}
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
before
do
project
.
team
<<
[
user
,
:master
]
login_as
(
user
)
end
scenario
'filters by no Milestone'
,
js:
true
do
create
(
:merge_request
,
:with_diffs
,
source_project:
project
)
create
(
:merge_request
,
:simple
,
source_project:
project
,
milestone:
milestone
)
...
...
spec/finders/issues_finder_spec.rb
View file @
7e0ef892
...
...
@@ -62,6 +62,22 @@ describe IssuesFinder do
expect
(
issues
).
to
eq
([
issue2
])
end
it
'returns unique issues when filtering by multiple labels'
do
label2
=
create
(
:label
,
project:
project2
)
create
(
:label_link
,
label:
label2
,
target:
issue2
)
params
=
{
scope:
'all'
,
label_name:
[
label
.
title
,
label2
.
title
].
join
(
','
),
state:
'opened'
}
issues
=
IssuesFinder
.
new
(
user
,
params
).
execute
expect
(
issues
).
to
eq
([
issue2
])
end
it
'should filter by no label name'
do
params
=
{
scope:
"all"
,
label_name:
Label
::
None
.
title
,
state:
'opened'
}
issues
=
IssuesFinder
.
new
(
user
,
params
).
execute
...
...
spec/lib/gitlab/metrics_spec.rb
View file @
7e0ef892
...
...
@@ -123,4 +123,28 @@ describe Gitlab::Metrics do
end
end
end
describe
'.action='
do
context
'without a transaction'
do
it
'does nothing'
do
expect_any_instance_of
(
Gitlab
::
Metrics
::
Transaction
).
not_to
receive
(
:action
=
)
Gitlab
::
Metrics
.
action
=
'foo'
end
end
context
'with a transaction'
do
it
'sets the action of a transaction'
do
trans
=
Gitlab
::
Metrics
::
Transaction
.
new
expect
(
Gitlab
::
Metrics
).
to
receive
(
:current_transaction
).
and_return
(
trans
)
expect
(
trans
).
to
receive
(
:action
=
).
with
(
'foo'
)
Gitlab
::
Metrics
.
action
=
'foo'
end
end
end
end
vendor/assets/javascripts/raven.js
0 → 100644
View file @
7e0ef892
/*! Raven.js 2.3.0 (b09d766) | github.com/getsentry/raven-js */
/*
* Includes TraceKit
* https://github.com/getsentry/TraceKit
*
* Copyright 2016 Matt Robenolt and other contributors
* Released under the BSD license
* https://github.com/getsentry/raven-js/blob/master/LICENSE
*
*/
(
function
(
f
){
if
(
typeof
exports
===
"
object
"
&&
typeof
module
!==
"
undefined
"
){
module
.
exports
=
f
()}
else
if
(
typeof
define
===
"
function
"
&&
define
.
amd
){
define
([],
f
)}
else
{
var
g
;
if
(
typeof
window
!==
"
undefined
"
){
g
=
window
}
else
if
(
typeof
global
!==
"
undefined
"
){
g
=
global
}
else
if
(
typeof
self
!==
"
undefined
"
){
g
=
self
}
else
{
g
=
this
}
g
.
Raven
=
f
()}})(
function
(){
var
define
,
module
,
exports
;
return
(
function
e
(
t
,
n
,
r
){
function
s
(
o
,
u
){
if
(
!
n
[
o
]){
if
(
!
t
[
o
]){
var
a
=
typeof
require
==
"
function
"
&&
require
;
if
(
!
u
&&
a
)
return
a
(
o
,
!
0
);
if
(
i
)
return
i
(
o
,
!
0
);
var
f
=
new
Error
(
"
Cannot find module '
"
+
o
+
"
'
"
);
throw
f
.
code
=
"
MODULE_NOT_FOUND
"
,
f
}
var
l
=
n
[
o
]
=
{
exports
:{}};
t
[
o
][
0
].
call
(
l
.
exports
,
function
(
e
){
var
n
=
t
[
o
][
1
][
e
];
return
s
(
n
?
n
:
e
)},
l
,
l
.
exports
,
e
,
t
,
n
,
r
)}
return
n
[
o
].
exports
}
var
i
=
typeof
require
==
"
function
"
&&
require
;
for
(
var
o
=
0
;
o
<
r
.
length
;
o
++
)
s
(
r
[
o
]);
return
s
})({
1
:[
function
(
_dereq_
,
module
,
exports
){
'
use strict
'
;
function
RavenConfigError
(
message
)
{
this
.
name
=
'
RavenConfigError
'
;
this
.
message
=
message
;
}
RavenConfigError
.
prototype
=
new
Error
();
RavenConfigError
.
prototype
.
constructor
=
RavenConfigError
;
module
.
exports
=
RavenConfigError
;
},{}],
2
:[
function
(
_dereq_
,
module
,
exports
){
/*global XDomainRequest:false*/
'
use strict
'
;
var
TraceKit
=
_dereq_
(
5
);
var
RavenConfigError
=
_dereq_
(
1
);
var
utils
=
_dereq_
(
4
);
var
isFunction
=
utils
.
isFunction
;
var
isUndefined
=
utils
.
isUndefined
;
var
isError
=
utils
.
isError
;
var
isEmptyObject
=
utils
.
isEmptyObject
;
var
hasKey
=
utils
.
hasKey
;
var
joinRegExp
=
utils
.
joinRegExp
;
var
each
=
utils
.
each
;
var
objectMerge
=
utils
.
objectMerge
;
var
truncate
=
utils
.
truncate
;
var
urlencode
=
utils
.
urlencode
;
var
uuid4
=
utils
.
uuid4
;
var
dsnKeys
=
'
source protocol user pass host port path
'
.
split
(
'
'
),
dsnPattern
=
/^
(?:(\w
+
)
:
)?\/\/(?:(\w
+
)(
:
\w
+
)?
@
)?([\w\.
-
]
+
)(?:
:
(\d
+
))?(\/
.*
)
/
;
function
now
()
{
return
+
new
Date
();
}
// First, check for JSON support
// If there is no JSON, we no-op the core features of Raven
// since JSON is required to encode the payload
function
Raven
()
{
this
.
_hasJSON
=
!!
(
typeof
JSON
===
'
object
'
&&
JSON
.
stringify
);
// Raven can run in contexts where there's no document (react-native)
this
.
_hasDocument
=
typeof
document
!==
'
undefined
'
;
this
.
_lastCapturedException
=
null
;
this
.
_lastEventId
=
null
;
this
.
_globalServer
=
null
;
this
.
_globalKey
=
null
;
this
.
_globalProject
=
null
;
this
.
_globalContext
=
{};
this
.
_globalOptions
=
{
logger
:
'
javascript
'
,
ignoreErrors
:
[],
ignoreUrls
:
[],
whitelistUrls
:
[],
includePaths
:
[],
crossOrigin
:
'
anonymous
'
,
collectWindowErrors
:
true
,
maxMessageLength
:
0
,
stackTraceLimit
:
50
};
this
.
_ignoreOnError
=
0
;
this
.
_isRavenInstalled
=
false
;
this
.
_originalErrorStackTraceLimit
=
Error
.
stackTraceLimit
;
// capture references to window.console *and* all its methods first
// before the console plugin has a chance to monkey patch
this
.
_originalConsole
=
window
.
console
||
{};
this
.
_originalConsoleMethods
=
{};
this
.
_plugins
=
[];
this
.
_startTime
=
now
();
this
.
_wrappedBuiltIns
=
[];
for
(
var
method
in
this
.
_originalConsole
)
{
// eslint-disable-line guard-for-in
this
.
_originalConsoleMethods
[
method
]
=
this
.
_originalConsole
[
method
];
}
}
/*
* The core Raven singleton
*
* @this {Raven}
*/
Raven
.
prototype
=
{
// Hardcode version string so that raven source can be loaded directly via
// webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465
VERSION
:
'
2.3.0
'
,
debug
:
false
,
TraceKit
:
TraceKit
,
// alias to TraceKit
/*
* Configure Raven with a DSN and extra options
*
* @param {string} dsn The public Sentry DSN
* @param {object} options Optional set of of global options [optional]
* @return {Raven}
*/
config
:
function
(
dsn
,
options
)
{
var
self
=
this
;
if
(
this
.
_globalServer
)
{
this
.
_logDebug
(
'
error
'
,
'
Error: Raven has already been configured
'
);
return
this
;
}
if
(
!
dsn
)
return
this
;
// merge in options
if
(
options
)
{
each
(
options
,
function
(
key
,
value
){
// tags and extra are special and need to be put into context
if
(
key
===
'
tags
'
||
key
===
'
extra
'
)
{
self
.
_globalContext
[
key
]
=
value
;
}
else
{
self
.
_globalOptions
[
key
]
=
value
;
}
});
}
var
uri
=
this
.
_parseDSN
(
dsn
),
lastSlash
=
uri
.
path
.
lastIndexOf
(
'
/
'
),
path
=
uri
.
path
.
substr
(
1
,
lastSlash
);
this
.
_dsn
=
dsn
;
// "Script error." is hard coded into browsers for errors that it can't read.
// this is the result of a script being pulled in from an external domain and CORS.
this
.
_globalOptions
.
ignoreErrors
.
push
(
/^Script error
\.?
$/
);
this
.
_globalOptions
.
ignoreErrors
.
push
(
/^Javascript error: Script error
\.?
on line 0$/
);
// join regexp rules into one big rule
this
.
_globalOptions
.
ignoreErrors
=
joinRegExp
(
this
.
_globalOptions
.
ignoreErrors
);
this
.
_globalOptions
.
ignoreUrls
=
this
.
_globalOptions
.
ignoreUrls
.
length
?
joinRegExp
(
this
.
_globalOptions
.
ignoreUrls
)
:
false
;
this
.
_globalOptions
.
whitelistUrls
=
this
.
_globalOptions
.
whitelistUrls
.
length
?
joinRegExp
(
this
.
_globalOptions
.
whitelistUrls
)
:
false
;
this
.
_globalOptions
.
includePaths
=
joinRegExp
(
this
.
_globalOptions
.
includePaths
);
this
.
_globalKey
=
uri
.
user
;
this
.
_globalSecret
=
uri
.
pass
&&
uri
.
pass
.
substr
(
1
);
this
.
_globalProject
=
uri
.
path
.
substr
(
lastSlash
+
1
);
this
.
_globalServer
=
this
.
_getGlobalServer
(
uri
);
this
.
_globalEndpoint
=
this
.
_globalServer
+
'
/
'
+
path
+
'
api/
'
+
this
.
_globalProject
+
'
/store/
'
;
if
(
this
.
_globalOptions
.
fetchContext
)
{
TraceKit
.
remoteFetching
=
true
;
}
if
(
this
.
_globalOptions
.
linesOfContext
)
{
TraceKit
.
linesOfContext
=
this
.
_globalOptions
.
linesOfContext
;
}
TraceKit
.
collectWindowErrors
=
!!
this
.
_globalOptions
.
collectWindowErrors
;
// return for chaining
return
this
;
},
/*
* Installs a global window.onerror error handler
* to capture and report uncaught exceptions.
* At this point, install() is required to be called due
* to the way TraceKit is set up.
*
* @return {Raven}
*/
install
:
function
()
{
var
self
=
this
;
if
(
this
.
isSetup
()
&&
!
this
.
_isRavenInstalled
)
{
TraceKit
.
report
.
subscribe
(
function
()
{
self
.
_handleOnErrorStackInfo
.
apply
(
self
,
arguments
);
});
this
.
_wrapBuiltIns
();
// Install all of the plugins
this
.
_drainPlugins
();
this
.
_isRavenInstalled
=
true
;
}
Error
.
stackTraceLimit
=
this
.
_globalOptions
.
stackTraceLimit
;
return
this
;
},
/*
* Wrap code within a context so Raven can capture errors
* reliably across domains that is executed immediately.
*
* @param {object} options A specific set of options for this context [optional]
* @param {function} func The callback to be immediately executed within the context
* @param {array} args An array of arguments to be called with the callback [optional]
*/
context
:
function
(
options
,
func
,
args
)
{
if
(
isFunction
(
options
))
{
args
=
func
||
[];
func
=
options
;
options
=
undefined
;
}
return
this
.
wrap
(
options
,
func
).
apply
(
this
,
args
);
},
/*
* Wrap code within a context and returns back a new function to be executed
*
* @param {object} options A specific set of options for this context [optional]
* @param {function} func The function to be wrapped in a new context
* @return {function} The newly wrapped functions with a context
*/
wrap
:
function
(
options
,
func
)
{
var
self
=
this
;
// 1 argument has been passed, and it's not a function
// so just return it
if
(
isUndefined
(
func
)
&&
!
isFunction
(
options
))
{
return
options
;
}
// options is optional
if
(
isFunction
(
options
))
{
func
=
options
;
options
=
undefined
;
}
// At this point, we've passed along 2 arguments, and the second one
// is not a function either, so we'll just return the second argument.
if
(
!
isFunction
(
func
))
{
return
func
;
}
// We don't wanna wrap it twice!
try
{
if
(
func
.
__raven__
)
{
return
func
;
}
}
catch
(
e
)
{
// Just accessing the __raven__ prop in some Selenium environments
// can cause a "Permission denied" exception (see raven-js#495).
// Bail on wrapping and return the function as-is (defers to window.onerror).
return
func
;
}
// If this has already been wrapped in the past, return that
if
(
func
.
__raven_wrapper__
){
return
func
.
__raven_wrapper__
;
}
function
wrapped
()
{
var
args
=
[],
i
=
arguments
.
length
,
deep
=
!
options
||
options
&&
options
.
deep
!==
false
;
// Recursively wrap all of a function's arguments that are
// functions themselves.
while
(
i
--
)
args
[
i
]
=
deep
?
self
.
wrap
(
options
,
arguments
[
i
])
:
arguments
[
i
];
try
{
return
func
.
apply
(
this
,
args
);
}
catch
(
e
)
{
self
.
_ignoreNextOnError
();
self
.
captureException
(
e
,
options
);
throw
e
;
}
}
// copy over properties of the old function
for
(
var
property
in
func
)
{
if
(
hasKey
(
func
,
property
))
{
wrapped
[
property
]
=
func
[
property
];
}
}
func
.
__raven_wrapper__
=
wrapped
;
wrapped
.
prototype
=
func
.
prototype
;
// Signal that this function has been wrapped already
// for both debugging and to prevent it to being wrapped twice
wrapped
.
__raven__
=
true
;
wrapped
.
__inner__
=
func
;
return
wrapped
;
},
/*
* Uninstalls the global error handler.
*
* @return {Raven}
*/
uninstall
:
function
()
{
TraceKit
.
report
.
uninstall
();
this
.
_restoreBuiltIns
();
Error
.
stackTraceLimit
=
this
.
_originalErrorStackTraceLimit
;
this
.
_isRavenInstalled
=
false
;
return
this
;
},
/*
* Manually capture an exception and send it over to Sentry
*
* @param {error} ex An exception to be logged
* @param {object} options A specific set of options for this error [optional]
* @return {Raven}
*/
captureException
:
function
(
ex
,
options
)
{
// If not an Error is passed through, recall as a message instead
if
(
!
isError
(
ex
))
return
this
.
captureMessage
(
ex
,
options
);
// Store the raw exception object for potential debugging and introspection
this
.
_lastCapturedException
=
ex
;
// TraceKit.report will re-raise any exception passed to it,
// which means you have to wrap it in try/catch. Instead, we
// can wrap it here and only re-raise if TraceKit.report
// raises an exception different from the one we asked to
// report on.
try
{
var
stack
=
TraceKit
.
computeStackTrace
(
ex
);
this
.
_handleStackInfo
(
stack
,
options
);
}
catch
(
ex1
)
{
if
(
ex
!==
ex1
)
{
throw
ex1
;
}
}
return
this
;
},
/*
* Manually send a message to Sentry
*
* @param {string} msg A plain message to be captured in Sentry
* @param {object} options A specific set of options for this message [optional]
* @return {Raven}
*/
captureMessage
:
function
(
msg
,
options
)
{
// config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an
// early call; we'll error on the side of logging anything called before configuration since it's
// probably something you should see:
if
(
!!
this
.
_globalOptions
.
ignoreErrors
.
test
&&
this
.
_globalOptions
.
ignoreErrors
.
test
(
msg
))
{
return
;
}
// Fire away!
this
.
_send
(
objectMerge
({
message
:
msg
+
''
// Make sure it's actually a string
},
options
)
);
return
this
;
},
addPlugin
:
function
(
plugin
/*arg1, arg2, ... argN*/
)
{
var
pluginArgs
=
Array
.
prototype
.
slice
.
call
(
arguments
,
1
);
this
.
_plugins
.
push
([
plugin
,
pluginArgs
]);
if
(
this
.
_isRavenInstalled
)
{
this
.
_drainPlugins
();
}
return
this
;
},
/*
* Set/clear a user to be sent along with the payload.
*
* @param {object} user An object representing user data [optional]
* @return {Raven}
*/
setUserContext
:
function
(
user
)
{
// Intentionally do not merge here since that's an unexpected behavior.
this
.
_globalContext
.
user
=
user
;
return
this
;
},
/*
* Merge extra attributes to be sent along with the payload.
*
* @param {object} extra An object representing extra data [optional]
* @return {Raven}
*/
setExtraContext
:
function
(
extra
)
{
this
.
_mergeContext
(
'
extra
'
,
extra
);
return
this
;
},
/*
* Merge tags to be sent along with the payload.
*
* @param {object} tags An object representing tags [optional]
* @return {Raven}
*/
setTagsContext
:
function
(
tags
)
{
this
.
_mergeContext
(
'
tags
'
,
tags
);
return
this
;
},
/*
* Clear all of the context.
*
* @return {Raven}
*/
clearContext
:
function
()
{
this
.
_globalContext
=
{};
return
this
;
},
/*
* Get a copy of the current context. This cannot be mutated.
*
* @return {object} copy of context
*/
getContext
:
function
()
{
// lol javascript
return
JSON
.
parse
(
JSON
.
stringify
(
this
.
_globalContext
));
},
/*
* Set release version of application
*
* @param {string} release Typically something like a git SHA to identify version
* @return {Raven}
*/
setRelease
:
function
(
release
)
{
this
.
_globalOptions
.
release
=
release
;
return
this
;
},
/*
* Set the dataCallback option
*
* @param {function} callback The callback to run which allows the
* data blob to be mutated before sending
* @return {Raven}
*/
setDataCallback
:
function
(
callback
)
{
this
.
_globalOptions
.
dataCallback
=
callback
;
return
this
;
},
/*
* Set the shouldSendCallback option
*
* @param {function} callback The callback to run which allows
* introspecting the blob before sending
* @return {Raven}
*/
setShouldSendCallback
:
function
(
callback
)
{
this
.
_globalOptions
.
shouldSendCallback
=
callback
;
return
this
;
},
/**
* Override the default HTTP transport mechanism that transmits data
* to the Sentry server.
*
* @param {function} transport Function invoked instead of the default
* `makeRequest` handler.
*
* @return {Raven}
*/
setTransport
:
function
(
transport
)
{
this
.
_globalOptions
.
transport
=
transport
;
return
this
;
},
/*
* Get the latest raw exception that was captured by Raven.
*
* @return {error}
*/
lastException
:
function
()
{
return
this
.
_lastCapturedException
;
},
/*
* Get the last event id
*
* @return {string}
*/
lastEventId
:
function
()
{
return
this
.
_lastEventId
;
},
/*
* Determine if Raven is setup and ready to go.
*
* @return {boolean}
*/
isSetup
:
function
()
{
if
(
!
this
.
_hasJSON
)
return
false
;
// needs JSON support
if
(
!
this
.
_globalServer
)
{
if
(
!
this
.
ravenNotConfiguredError
)
{
this
.
ravenNotConfiguredError
=
true
;
this
.
_logDebug
(
'
error
'
,
'
Error: Raven has not been configured.
'
);
}
return
false
;
}
return
true
;
},
afterLoad
:
function
()
{
// TODO: remove window dependence?
// Attempt to initialize Raven on load
var
RavenConfig
=
window
.
RavenConfig
;
if
(
RavenConfig
)
{
this
.
config
(
RavenConfig
.
dsn
,
RavenConfig
.
config
).
install
();
}
},
showReportDialog
:
function
(
options
)
{
if
(
!
window
.
document
)
// doesn't work without a document (React native)
return
;
options
=
options
||
{};
var
lastEventId
=
options
.
eventId
||
this
.
lastEventId
();
if
(
!
lastEventId
)
{
throw
new
RavenConfigError
(
'
Missing eventId
'
);
}
var
dsn
=
options
.
dsn
||
this
.
_dsn
;
if
(
!
dsn
)
{
throw
new
RavenConfigError
(
'
Missing DSN
'
);
}
var
encode
=
encodeURIComponent
;
var
qs
=
''
;
qs
+=
'
?eventId=
'
+
encode
(
lastEventId
);
qs
+=
'
&dsn=
'
+
encode
(
dsn
);
var
user
=
options
.
user
||
this
.
_globalContext
.
user
;
if
(
user
)
{
if
(
user
.
name
)
qs
+=
'
&name=
'
+
encode
(
user
.
name
);
if
(
user
.
email
)
qs
+=
'
&email=
'
+
encode
(
user
.
email
);
}
var
globalServer
=
this
.
_getGlobalServer
(
this
.
_parseDSN
(
dsn
));
var
script
=
document
.
createElement
(
'
script
'
);
script
.
async
=
true
;
script
.
src
=
globalServer
+
'
/api/embed/error-page/
'
+
qs
;
(
document
.
head
||
document
.
body
).
appendChild
(
script
);
},
/**** Private functions ****/
_ignoreNextOnError
:
function
()
{
var
self
=
this
;
this
.
_ignoreOnError
+=
1
;
setTimeout
(
function
()
{
// onerror should trigger before setTimeout
self
.
_ignoreOnError
-=
1
;
});
},
_triggerEvent
:
function
(
eventType
,
options
)
{
// NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it
var
evt
,
key
;
if
(
!
this
.
_hasDocument
)
return
;
options
=
options
||
{};
eventType
=
'
raven
'
+
eventType
.
substr
(
0
,
1
).
toUpperCase
()
+
eventType
.
substr
(
1
);
if
(
document
.
createEvent
)
{
evt
=
document
.
createEvent
(
'
HTMLEvents
'
);
evt
.
initEvent
(
eventType
,
true
,
true
);
}
else
{
evt
=
document
.
createEventObject
();
evt
.
eventType
=
eventType
;
}
for
(
key
in
options
)
if
(
hasKey
(
options
,
key
))
{
evt
[
key
]
=
options
[
key
];
}
if
(
document
.
createEvent
)
{
// IE9 if standards
document
.
dispatchEvent
(
evt
);
}
else
{
// IE8 regardless of Quirks or Standards
// IE9 if quirks
try
{
document
.
fireEvent
(
'
on
'
+
evt
.
eventType
.
toLowerCase
(),
evt
);
}
catch
(
e
)
{
// Do nothing
}
}
},
/**
* Install any queued plugins
*/
_wrapBuiltIns
:
function
()
{
var
self
=
this
;
function
fill
(
obj
,
name
,
replacement
,
noUndo
)
{
var
orig
=
obj
[
name
];
obj
[
name
]
=
replacement
(
orig
);
if
(
!
noUndo
)
{
self
.
_wrappedBuiltIns
.
push
([
obj
,
name
,
orig
]);
}
}
function
wrapTimeFn
(
orig
)
{
return
function
(
fn
,
t
)
{
// preserve arity
// Make a copy of the arguments
var
args
=
[].
slice
.
call
(
arguments
);
var
originalCallback
=
args
[
0
];
if
(
isFunction
(
originalCallback
))
{
args
[
0
]
=
self
.
wrap
(
originalCallback
);
}
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
// also supports only two arguments and doesn't care what this is, so we
// can just call the original function directly.
if
(
orig
.
apply
)
{
return
orig
.
apply
(
this
,
args
);
}
else
{
return
orig
(
args
[
0
],
args
[
1
]);
}
};
}
fill
(
window
,
'
setTimeout
'
,
wrapTimeFn
);
fill
(
window
,
'
setInterval
'
,
wrapTimeFn
);
if
(
window
.
requestAnimationFrame
)
{
fill
(
window
,
'
requestAnimationFrame
'
,
function
(
orig
)
{
return
function
(
cb
)
{
return
orig
(
self
.
wrap
(
cb
));
};
});
}
// event targets borrowed from bugsnag-js:
// https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
'
EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload
'
.
replace
(
/
\w
+/g
,
function
(
global
)
{
var
proto
=
window
[
global
]
&&
window
[
global
].
prototype
;
if
(
proto
&&
proto
.
hasOwnProperty
&&
proto
.
hasOwnProperty
(
'
addEventListener
'
))
{
fill
(
proto
,
'
addEventListener
'
,
function
(
orig
)
{
return
function
(
evt
,
fn
,
capture
,
secure
)
{
// preserve arity
try
{
if
(
fn
&&
fn
.
handleEvent
)
{
fn
.
handleEvent
=
self
.
wrap
(
fn
.
handleEvent
);
}
}
catch
(
err
)
{
// can sometimes get 'Permission denied to access property "handle Event'
}
return
orig
.
call
(
this
,
evt
,
self
.
wrap
(
fn
),
capture
,
secure
);
};
});
fill
(
proto
,
'
removeEventListener
'
,
function
(
orig
)
{
return
function
(
evt
,
fn
,
capture
,
secure
)
{
fn
=
fn
&&
(
fn
.
__raven_wrapper__
?
fn
.
__raven_wrapper__
:
fn
);
return
orig
.
call
(
this
,
evt
,
fn
,
capture
,
secure
);
};
});
}
});
if
(
'
XMLHttpRequest
'
in
window
)
{
fill
(
XMLHttpRequest
.
prototype
,
'
send
'
,
function
(
origSend
)
{
return
function
(
data
)
{
// preserve arity
var
xhr
=
this
;
'
onreadystatechange onload onerror onprogress
'
.
replace
(
/
\w
+/g
,
function
(
prop
)
{
if
(
prop
in
xhr
&&
Object
.
prototype
.
toString
.
call
(
xhr
[
prop
])
===
'
[object Function]
'
)
{
fill
(
xhr
,
prop
,
function
(
orig
)
{
return
self
.
wrap
(
orig
);
},
true
/* noUndo */
);
// don't track filled methods on XHR instances
}
});
return
origSend
.
apply
(
this
,
arguments
);
};
});
}
var
$
=
window
.
jQuery
||
window
.
$
;
if
(
$
&&
$
.
fn
&&
$
.
fn
.
ready
)
{
fill
(
$
.
fn
,
'
ready
'
,
function
(
orig
)
{
return
function
(
fn
)
{
return
orig
.
call
(
this
,
self
.
wrap
(
fn
));
};
});
}
},
_restoreBuiltIns
:
function
()
{
// restore any wrapped builtins
var
builtin
;
while
(
this
.
_wrappedBuiltIns
.
length
)
{
builtin
=
this
.
_wrappedBuiltIns
.
shift
();
var
obj
=
builtin
[
0
],
name
=
builtin
[
1
],
orig
=
builtin
[
2
];
obj
[
name
]
=
orig
;
}
},
_drainPlugins
:
function
()
{
var
self
=
this
;
// FIX ME TODO
each
(
this
.
_plugins
,
function
(
_
,
plugin
)
{
var
installer
=
plugin
[
0
];
var
args
=
plugin
[
1
];
installer
.
apply
(
self
,
[
self
].
concat
(
args
));
});
},
_parseDSN
:
function
(
str
)
{
var
m
=
dsnPattern
.
exec
(
str
),
dsn
=
{},
i
=
7
;
try
{
while
(
i
--
)
dsn
[
dsnKeys
[
i
]]
=
m
[
i
]
||
''
;
}
catch
(
e
)
{
throw
new
RavenConfigError
(
'
Invalid DSN:
'
+
str
);
}
if
(
dsn
.
pass
&&
!
this
.
_globalOptions
.
allowSecretKey
)
{
throw
new
RavenConfigError
(
'
Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key
'
);
}
return
dsn
;
},
_getGlobalServer
:
function
(
uri
)
{
// assemble the endpoint from the uri pieces
var
globalServer
=
'
//
'
+
uri
.
host
+
(
uri
.
port
?
'
:
'
+
uri
.
port
:
''
);
if
(
uri
.
protocol
)
{
globalServer
=
uri
.
protocol
+
'
:
'
+
globalServer
;
}
return
globalServer
;
},
_handleOnErrorStackInfo
:
function
()
{
// if we are intentionally ignoring errors via onerror, bail out
if
(
!
this
.
_ignoreOnError
)
{
this
.
_handleStackInfo
.
apply
(
this
,
arguments
);
}
},
_handleStackInfo
:
function
(
stackInfo
,
options
)
{
var
self
=
this
;
var
frames
=
[];
if
(
stackInfo
.
stack
&&
stackInfo
.
stack
.
length
)
{
each
(
stackInfo
.
stack
,
function
(
i
,
stack
)
{
var
frame
=
self
.
_normalizeFrame
(
stack
);
if
(
frame
)
{
frames
.
push
(
frame
);
}
});
}
this
.
_triggerEvent
(
'
handle
'
,
{
stackInfo
:
stackInfo
,
options
:
options
});
this
.
_processException
(
stackInfo
.
name
,
stackInfo
.
message
,
stackInfo
.
url
,
stackInfo
.
lineno
,
frames
.
slice
(
0
,
this
.
_globalOptions
.
stackTraceLimit
),
options
);
},
_normalizeFrame
:
function
(
frame
)
{
if
(
!
frame
.
url
)
return
;
// normalize the frames data
var
normalized
=
{
filename
:
frame
.
url
,
lineno
:
frame
.
line
,
colno
:
frame
.
column
,
'
function
'
:
frame
.
func
||
'
?
'
},
context
=
this
.
_extractContextFromFrame
(
frame
),
i
;
if
(
context
)
{
var
keys
=
[
'
pre_context
'
,
'
context_line
'
,
'
post_context
'
];
i
=
3
;
while
(
i
--
)
normalized
[
keys
[
i
]]
=
context
[
i
];
}
normalized
.
in_app
=
!
(
// determine if an exception came from outside of our app
// first we check the global includePaths list.
!!
this
.
_globalOptions
.
includePaths
.
test
&&
!
this
.
_globalOptions
.
includePaths
.
test
(
normalized
.
filename
)
||
// Now we check for fun, if the function name is Raven or TraceKit
/
(
Raven|TraceKit
)\.
/
.
test
(
normalized
[
'
function
'
])
||
// finally, we do a last ditch effort and check for raven.min.js
/raven
\.(
min
\.)?
js$/
.
test
(
normalized
.
filename
)
);
return
normalized
;
},
_extractContextFromFrame
:
function
(
frame
)
{
// immediately check if we should even attempt to parse a context
if
(
!
frame
.
context
||
!
this
.
_globalOptions
.
fetchContext
)
return
;
var
context
=
frame
.
context
,
pivot
=
~~
(
context
.
length
/
2
),
i
=
context
.
length
,
isMinified
=
false
;
while
(
i
--
)
{
// We're making a guess to see if the source is minified or not.
// To do that, we make the assumption if *any* of the lines passed
// in are greater than 300 characters long, we bail.
// Sentry will see that there isn't a context
if
(
context
[
i
].
length
>
300
)
{
isMinified
=
true
;
break
;
}
}
if
(
isMinified
)
{
// The source is minified and we don't know which column. Fuck it.
if
(
isUndefined
(
frame
.
column
))
return
;
// If the source is minified and has a frame column
// we take a chunk of the offending line to hopefully shed some light
return
[
[],
// no pre_context
context
[
pivot
].
substr
(
frame
.
column
,
50
),
// grab 50 characters, starting at the offending column
[]
// no post_context
];
}
return
[
context
.
slice
(
0
,
pivot
),
// pre_context
context
[
pivot
],
// context_line
context
.
slice
(
pivot
+
1
)
// post_context
];
},
_processException
:
function
(
type
,
message
,
fileurl
,
lineno
,
frames
,
options
)
{
var
stacktrace
,
fullMessage
;
if
(
!!
this
.
_globalOptions
.
ignoreErrors
.
test
&&
this
.
_globalOptions
.
ignoreErrors
.
test
(
message
))
return
;
message
+=
''
;
message
=
truncate
(
message
,
this
.
_globalOptions
.
maxMessageLength
);
fullMessage
=
(
type
?
type
+
'
:
'
:
''
)
+
message
;
fullMessage
=
truncate
(
fullMessage
,
this
.
_globalOptions
.
maxMessageLength
);
if
(
frames
&&
frames
.
length
)
{
fileurl
=
frames
[
0
].
filename
||
fileurl
;
// Sentry expects frames oldest to newest
// and JS sends them as newest to oldest
frames
.
reverse
();
stacktrace
=
{
frames
:
frames
};
}
else
if
(
fileurl
)
{
stacktrace
=
{
frames
:
[{
filename
:
fileurl
,
lineno
:
lineno
,
in_app
:
true
}]
};
}
if
(
!!
this
.
_globalOptions
.
ignoreUrls
.
test
&&
this
.
_globalOptions
.
ignoreUrls
.
test
(
fileurl
))
return
;
if
(
!!
this
.
_globalOptions
.
whitelistUrls
.
test
&&
!
this
.
_globalOptions
.
whitelistUrls
.
test
(
fileurl
))
return
;
var
data
=
objectMerge
({
// sentry.interfaces.Exception
exception
:
{
values
:
[{
type
:
type
,
value
:
message
,
stacktrace
:
stacktrace
}]
},
culprit
:
fileurl
,
message
:
fullMessage
},
options
);
// Fire away!
this
.
_send
(
data
);
},
_trimPacket
:
function
(
data
)
{
// For now, we only want to truncate the two different messages
// but this could/should be expanded to just trim everything
var
max
=
this
.
_globalOptions
.
maxMessageLength
;
data
.
message
=
truncate
(
data
.
message
,
max
);
if
(
data
.
exception
)
{
var
exception
=
data
.
exception
.
values
[
0
];
exception
.
value
=
truncate
(
exception
.
value
,
max
);
}
return
data
;
},
_getHttpData
:
function
()
{
if
(
!
this
.
_hasDocument
||
!
document
.
location
||
!
document
.
location
.
href
)
{
return
;
}
var
httpData
=
{
headers
:
{
'
User-Agent
'
:
navigator
.
userAgent
}
};
httpData
.
url
=
document
.
location
.
href
;
if
(
document
.
referrer
)
{
httpData
.
headers
.
Referer
=
document
.
referrer
;
}
return
httpData
;
},
_send
:
function
(
data
)
{
var
self
=
this
;
var
globalOptions
=
this
.
_globalOptions
;
var
baseData
=
{
project
:
this
.
_globalProject
,
logger
:
globalOptions
.
logger
,
platform
:
'
javascript
'
},
httpData
=
this
.
_getHttpData
();
if
(
httpData
)
{
baseData
.
request
=
httpData
;
}
data
=
objectMerge
(
baseData
,
data
);
// Merge in the tags and extra separately since objectMerge doesn't handle a deep merge
data
.
tags
=
objectMerge
(
objectMerge
({},
this
.
_globalContext
.
tags
),
data
.
tags
);
data
.
extra
=
objectMerge
(
objectMerge
({},
this
.
_globalContext
.
extra
),
data
.
extra
);
// Send along our own collected metadata with extra
data
.
extra
[
'
session:duration
'
]
=
now
()
-
this
.
_startTime
;
// If there are no tags/extra, strip the key from the payload alltogther.
if
(
isEmptyObject
(
data
.
tags
))
delete
data
.
tags
;
if
(
this
.
_globalContext
.
user
)
{
// sentry.interfaces.User
data
.
user
=
this
.
_globalContext
.
user
;
}
// Include the release if it's defined in globalOptions
if
(
globalOptions
.
release
)
data
.
release
=
globalOptions
.
release
;
// Include server_name if it's defined in globalOptions
if
(
globalOptions
.
serverName
)
data
.
server_name
=
globalOptions
.
serverName
;
if
(
isFunction
(
globalOptions
.
dataCallback
))
{
data
=
globalOptions
.
dataCallback
(
data
)
||
data
;
}
// Why??????????
if
(
!
data
||
isEmptyObject
(
data
))
{
return
;
}
// Check if the request should be filtered or not
if
(
isFunction
(
globalOptions
.
shouldSendCallback
)
&&
!
globalOptions
.
shouldSendCallback
(
data
))
{
return
;
}
// Send along an event_id if not explicitly passed.
// This event_id can be used to reference the error within Sentry itself.
// Set lastEventId after we know the error should actually be sent
this
.
_lastEventId
=
data
.
event_id
||
(
data
.
event_id
=
uuid4
());
// Try and clean up the packet before sending by truncating long values
data
=
this
.
_trimPacket
(
data
);
this
.
_logDebug
(
'
debug
'
,
'
Raven about to send:
'
,
data
);
if
(
!
this
.
isSetup
())
return
;
var
auth
=
{
sentry_version
:
'
7
'
,
sentry_client
:
'
raven-js/
'
+
this
.
VERSION
,
sentry_key
:
this
.
_globalKey
};
if
(
this
.
_globalSecret
)
{
auth
.
sentry_secret
=
this
.
_globalSecret
;
}
var
url
=
this
.
_globalEndpoint
;
(
globalOptions
.
transport
||
this
.
_makeRequest
).
call
(
this
,
{
url
:
url
,
auth
:
auth
,
data
:
data
,
options
:
globalOptions
,
onSuccess
:
function
success
()
{
self
.
_triggerEvent
(
'
success
'
,
{
data
:
data
,
src
:
url
});
},
onError
:
function
failure
()
{
self
.
_triggerEvent
(
'
failure
'
,
{
data
:
data
,
src
:
url
});
}
});
},
_makeImageRequest
:
function
(
opts
)
{
// Tack on sentry_data to auth options, which get urlencoded
opts
.
auth
.
sentry_data
=
JSON
.
stringify
(
opts
.
data
);
var
img
=
this
.
_newImage
(),
src
=
opts
.
url
+
'
?
'
+
urlencode
(
opts
.
auth
),
crossOrigin
=
opts
.
options
.
crossOrigin
;
if
(
crossOrigin
||
crossOrigin
===
''
)
{
img
.
crossOrigin
=
crossOrigin
;
}
img
.
onload
=
opts
.
onSuccess
;
img
.
onerror
=
img
.
onabort
=
opts
.
onError
;
img
.
src
=
src
;
},
_makeXhrRequest
:
function
(
opts
)
{
var
request
;
var
url
=
opts
.
url
;
function
handler
()
{
if
(
request
.
status
===
200
)
{
if
(
opts
.
onSuccess
)
{
opts
.
onSuccess
();
}
}
else
if
(
opts
.
onError
)
{
opts
.
onError
();
}
}
request
=
new
XMLHttpRequest
();
if
(
'
withCredentials
'
in
request
)
{
request
.
onreadystatechange
=
function
()
{
if
(
request
.
readyState
!==
4
)
{
return
;
}
handler
();
};
}
else
{
request
=
new
XDomainRequest
();
// xdomainrequest cannot go http -> https (or vice versa),
// so always use protocol relative
url
=
url
.
replace
(
/^https
?
:/
,
''
);
// onreadystatechange not supported by XDomainRequest
request
.
onload
=
handler
;
}
// NOTE: auth is intentionally sent as part of query string (NOT as custom
// HTTP header) so as to avoid preflight CORS requests
request
.
open
(
'
POST
'
,
url
+
'
?
'
+
urlencode
(
opts
.
auth
));
request
.
send
(
JSON
.
stringify
(
opts
.
data
));
},
_makeRequest
:
function
(
opts
)
{
var
hasCORS
=
'
withCredentials
'
in
new
XMLHttpRequest
()
||
typeof
XDomainRequest
!==
'
undefined
'
;
return
(
hasCORS
?
this
.
_makeXhrRequest
:
this
.
_makeImageRequest
)(
opts
);
},
// Note: this is shitty, but I can't figure out how to get
// sinon to stub document.createElement without breaking everything
// so this wrapper is just so I can stub it for tests.
_newImage
:
function
()
{
return
document
.
createElement
(
'
img
'
);
},
_logDebug
:
function
(
level
)
{
if
(
this
.
_originalConsoleMethods
[
level
]
&&
this
.
debug
)
{
// In IE<10 console methods do not have their own 'apply' method
Function
.
prototype
.
apply
.
call
(
this
.
_originalConsoleMethods
[
level
],
this
.
_originalConsole
,
[].
slice
.
call
(
arguments
,
1
)
);
}
},
_mergeContext
:
function
(
key
,
context
)
{
if
(
isUndefined
(
context
))
{
delete
this
.
_globalContext
[
key
];
}
else
{
this
.
_globalContext
[
key
]
=
objectMerge
(
this
.
_globalContext
[
key
]
||
{},
context
);
}
}
};
// Deprecations
Raven
.
prototype
.
setUser
=
Raven
.
prototype
.
setUserContext
;
Raven
.
prototype
.
setReleaseContext
=
Raven
.
prototype
.
setRelease
;
module
.
exports
=
Raven
;
},{
"
1
"
:
1
,
"
4
"
:
4
,
"
5
"
:
5
}],
3
:[
function
(
_dereq_
,
module
,
exports
){
/**
* Enforces a single instance of the Raven client, and the
* main entry point for Raven. If you are a consumer of the
* Raven library, you SHOULD load this file (vs raven.js).
**/
'
use strict
'
;
var
RavenConstructor
=
_dereq_
(
2
);
var
_Raven
=
window
.
Raven
;
var
Raven
=
new
RavenConstructor
();
/*
* Allow multiple versions of Raven to be installed.
* Strip Raven from the global context and returns the instance.
*
* @return {Raven}
*/
Raven
.
noConflict
=
function
()
{
window
.
Raven
=
_Raven
;
return
Raven
;
};
Raven
.
afterLoad
();
module
.
exports
=
Raven
;
},{
"
2
"
:
2
}],
4
:[
function
(
_dereq_
,
module
,
exports
){
'
use strict
'
;
var
objectPrototype
=
Object
.
prototype
;
function
isUndefined
(
what
)
{
return
what
===
void
0
;
}
function
isFunction
(
what
)
{
return
typeof
what
===
'
function
'
;
}
function
isString
(
what
)
{
return
objectPrototype
.
toString
.
call
(
what
)
===
'
[object String]
'
;
}
function
isObject
(
what
)
{
return
typeof
what
===
'
object
'
&&
what
!==
null
;
}
function
isEmptyObject
(
what
)
{
for
(
var
_
in
what
)
return
false
;
// eslint-disable-line guard-for-in, no-unused-vars
return
true
;
}
// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560
// with some tiny modifications
function
isError
(
what
)
{
var
toString
=
objectPrototype
.
toString
.
call
(
what
);
return
isObject
(
what
)
&&
toString
===
'
[object Error]
'
||
toString
===
'
[object Exception]
'
||
// Firefox NS_ERROR_FAILURE Exceptions
what
instanceof
Error
;
}
function
each
(
obj
,
callback
)
{
var
i
,
j
;
if
(
isUndefined
(
obj
.
length
))
{
for
(
i
in
obj
)
{
if
(
hasKey
(
obj
,
i
))
{
callback
.
call
(
null
,
i
,
obj
[
i
]);
}
}
}
else
{
j
=
obj
.
length
;
if
(
j
)
{
for
(
i
=
0
;
i
<
j
;
i
++
)
{
callback
.
call
(
null
,
i
,
obj
[
i
]);
}
}
}
}
function
objectMerge
(
obj1
,
obj2
)
{
if
(
!
obj2
)
{
return
obj1
;
}
each
(
obj2
,
function
(
key
,
value
){
obj1
[
key
]
=
value
;
});
return
obj1
;
}
function
truncate
(
str
,
max
)
{
return
!
max
||
str
.
length
<=
max
?
str
:
str
.
substr
(
0
,
max
)
+
'
\
u2026
'
;
}
/**
* hasKey, a better form of hasOwnProperty
* Example: hasKey(MainHostObject, property) === true/false
*
* @param {Object} host object to check property
* @param {string} key to check
*/
function
hasKey
(
object
,
key
)
{
return
objectPrototype
.
hasOwnProperty
.
call
(
object
,
key
);
}
function
joinRegExp
(
patterns
)
{
// Combine an array of regular expressions and strings into one large regexp
// Be mad.
var
sources
=
[],
i
=
0
,
len
=
patterns
.
length
,
pattern
;
for
(;
i
<
len
;
i
++
)
{
pattern
=
patterns
[
i
];
if
(
isString
(
pattern
))
{
// If it's a string, we need to escape it
// Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
sources
.
push
(
pattern
.
replace
(
/
([
.*+?^=!:${}()|
\[\]\/\\])
/g
,
'
\\
$1
'
));
}
else
if
(
pattern
&&
pattern
.
source
)
{
// If it's a regexp already, we want to extract the source
sources
.
push
(
pattern
.
source
);
}
// Intentionally skip other cases
}
return
new
RegExp
(
sources
.
join
(
'
|
'
),
'
i
'
);
}
function
urlencode
(
o
)
{
var
pairs
=
[];
each
(
o
,
function
(
key
,
value
)
{
pairs
.
push
(
encodeURIComponent
(
key
)
+
'
=
'
+
encodeURIComponent
(
value
));
});
return
pairs
.
join
(
'
&
'
);
}
function
uuid4
()
{
var
crypto
=
window
.
crypto
||
window
.
msCrypto
;
if
(
!
isUndefined
(
crypto
)
&&
crypto
.
getRandomValues
)
{
// Use window.crypto API if available
var
arr
=
new
Uint16Array
(
8
);
crypto
.
getRandomValues
(
arr
);
// set 4 in byte 7
arr
[
3
]
=
arr
[
3
]
&
0xFFF
|
0x4000
;
// set 2 most significant bits of byte 9 to '10'
arr
[
4
]
=
arr
[
4
]
&
0x3FFF
|
0x8000
;
var
pad
=
function
(
num
)
{
var
v
=
num
.
toString
(
16
);
while
(
v
.
length
<
4
)
{
v
=
'
0
'
+
v
;
}
return
v
;
};
return
pad
(
arr
[
0
])
+
pad
(
arr
[
1
])
+
pad
(
arr
[
2
])
+
pad
(
arr
[
3
])
+
pad
(
arr
[
4
])
+
pad
(
arr
[
5
])
+
pad
(
arr
[
6
])
+
pad
(
arr
[
7
]);
}
else
{
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
return
'
xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx
'
.
replace
(
/
[
xy
]
/g
,
function
(
c
)
{
var
r
=
Math
.
random
()
*
16
|
0
,
v
=
c
===
'
x
'
?
r
:
r
&
0x3
|
0x8
;
return
v
.
toString
(
16
);
});
}
}
module
.
exports
=
{
isUndefined
:
isUndefined
,
isFunction
:
isFunction
,
isString
:
isString
,
isObject
:
isObject
,
isEmptyObject
:
isEmptyObject
,
isError
:
isError
,
each
:
each
,
objectMerge
:
objectMerge
,
truncate
:
truncate
,
hasKey
:
hasKey
,
joinRegExp
:
joinRegExp
,
urlencode
:
urlencode
,
uuid4
:
uuid4
};
},{}],
5
:[
function
(
_dereq_
,
module
,
exports
){
'
use strict
'
;
var
utils
=
_dereq_
(
4
);
var
hasKey
=
utils
.
hasKey
;
var
isString
=
utils
.
isString
;
var
isUndefined
=
utils
.
isUndefined
;
/*
TraceKit - Cross brower stack traces - github.com/occ/TraceKit
MIT license
*/
var
TraceKit
=
{
remoteFetching
:
false
,
collectWindowErrors
:
true
,
// 3 lines before, the offending line, 3 lines after
linesOfContext
:
7
,
debug
:
false
};
// global reference to slice
var
_slice
=
[].
slice
;
var
UNKNOWN_FUNCTION
=
'
?
'
;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types
var
ERROR_TYPES_RE
=
/^
(?:
Uncaught
)?((?:
Eval|Internal|Range|Reference|Syntax|Type|URI
)
Error
)\:
?(
.*
)
$/
;
function
getLocationHref
()
{
if
(
typeof
document
===
'
undefined
'
)
return
''
;
return
document
.
location
.
href
;
}
/**
* TraceKit.report: cross-browser processing of unhandled exceptions
*
* Syntax:
* TraceKit.report.subscribe(function(stackInfo) { ... })
* TraceKit.report.unsubscribe(function(stackInfo) { ... })
* TraceKit.report(exception)
* try { ...code... } catch(ex) { TraceKit.report(ex); }
*
* Supports:
* - Firefox: full stack trace with line numbers, plus column number
* on top frame; column number is not guaranteed
* - Opera: full stack trace with line and column numbers
* - Chrome: full stack trace with line and column numbers
* - Safari: line and column number for the top frame only; some frames
* may be missing, and column number is not guaranteed
* - IE: line and column number for the top frame only; some frames
* may be missing, and column number is not guaranteed
*
* In theory, TraceKit should work on all of the following versions:
* - IE5.5+ (only 8.0 tested)
* - Firefox 0.9+ (only 3.5+ tested)
* - Opera 7+ (only 10.50 tested; versions 9 and earlier may require
* Exceptions Have Stacktrace to be enabled in opera:config)
* - Safari 3+ (only 4+ tested)
* - Chrome 1+ (only 5+ tested)
* - Konqueror 3.5+ (untested)
*
* Requires TraceKit.computeStackTrace.
*
* Tries to catch all unhandled exceptions and report them to the
* subscribed handlers. Please note that TraceKit.report will rethrow the
* exception. This is REQUIRED in order to get a useful stack trace in IE.
* If the exception does not reach the top of the browser, you will only
* get a stack trace from the point where TraceKit.report was called.
*
* Handlers receive a stackInfo object as described in the
* TraceKit.computeStackTrace docs.
*/
TraceKit
.
report
=
(
function
reportModuleWrapper
()
{
var
handlers
=
[],
lastArgs
=
null
,
lastException
=
null
,
lastExceptionStack
=
null
;
/**
* Add a crash handler.
* @param {Function} handler
*/
function
subscribe
(
handler
)
{
installGlobalHandler
();
handlers
.
push
(
handler
);
}
/**
* Remove a crash handler.
* @param {Function} handler
*/
function
unsubscribe
(
handler
)
{
for
(
var
i
=
handlers
.
length
-
1
;
i
>=
0
;
--
i
)
{
if
(
handlers
[
i
]
===
handler
)
{
handlers
.
splice
(
i
,
1
);
}
}
}
/**
* Remove all crash handlers.
*/
function
unsubscribeAll
()
{
uninstallGlobalHandler
();
handlers
=
[];
}
/**
* Dispatch stack information to all handlers.
* @param {Object.<string, *>} stack
*/
function
notifyHandlers
(
stack
,
isWindowError
)
{
var
exception
=
null
;
if
(
isWindowError
&&
!
TraceKit
.
collectWindowErrors
)
{
return
;
}
for
(
var
i
in
handlers
)
{
if
(
hasKey
(
handlers
,
i
))
{
try
{
handlers
[
i
].
apply
(
null
,
[
stack
].
concat
(
_slice
.
call
(
arguments
,
2
)));
}
catch
(
inner
)
{
exception
=
inner
;
}
}
}
if
(
exception
)
{
throw
exception
;
}
}
var
_oldOnerrorHandler
,
_onErrorHandlerInstalled
;
/**
* Ensures all global unhandled exceptions are recorded.
* Supported by Gecko and IE.
* @param {string} message Error message.
* @param {string} url URL of script that generated the exception.
* @param {(number|string)} lineNo The line number at which the error
* occurred.
* @param {?(number|string)} colNo The column number at which the error
* occurred.
* @param {?Error} ex The actual Error object.
*/
function
traceKitWindowOnError
(
message
,
url
,
lineNo
,
colNo
,
ex
)
{
var
stack
=
null
;
if
(
lastExceptionStack
)
{
TraceKit
.
computeStackTrace
.
augmentStackTraceWithInitialElement
(
lastExceptionStack
,
url
,
lineNo
,
message
);
processLastException
();
}
else
if
(
ex
)
{
// New chrome and blink send along a real error object
// Let's just report that like a normal error.
// See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
stack
=
TraceKit
.
computeStackTrace
(
ex
);
notifyHandlers
(
stack
,
true
);
}
else
{
var
location
=
{
'
url
'
:
url
,
'
line
'
:
lineNo
,
'
column
'
:
colNo
};
location
.
func
=
TraceKit
.
computeStackTrace
.
guessFunctionName
(
location
.
url
,
location
.
line
);
location
.
context
=
TraceKit
.
computeStackTrace
.
gatherContext
(
location
.
url
,
location
.
line
);
var
name
=
undefined
;
var
msg
=
message
;
// must be new var or will modify original `arguments`
var
groups
;
if
(
isString
(
message
))
{
var
groups
=
message
.
match
(
ERROR_TYPES_RE
);
if
(
groups
)
{
name
=
groups
[
1
];
msg
=
groups
[
2
];
}
}
stack
=
{
'
name
'
:
name
,
'
message
'
:
msg
,
'
url
'
:
getLocationHref
(),
'
stack
'
:
[
location
]
};
notifyHandlers
(
stack
,
true
);
}
if
(
_oldOnerrorHandler
)
{
return
_oldOnerrorHandler
.
apply
(
this
,
arguments
);
}
return
false
;
}
function
installGlobalHandler
()
{
if
(
_onErrorHandlerInstalled
)
{
return
;
}
_oldOnerrorHandler
=
window
.
onerror
;
window
.
onerror
=
traceKitWindowOnError
;
_onErrorHandlerInstalled
=
true
;
}
function
uninstallGlobalHandler
()
{
if
(
!
_onErrorHandlerInstalled
)
{
return
;
}
window
.
onerror
=
_oldOnerrorHandler
;
_onErrorHandlerInstalled
=
false
;
_oldOnerrorHandler
=
undefined
;
}
function
processLastException
()
{
var
_lastExceptionStack
=
lastExceptionStack
,
_lastArgs
=
lastArgs
;
lastArgs
=
null
;
lastExceptionStack
=
null
;
lastException
=
null
;
notifyHandlers
.
apply
(
null
,
[
_lastExceptionStack
,
false
].
concat
(
_lastArgs
));
}
/**
* Reports an unhandled Error to TraceKit.
* @param {Error} ex
* @param {?boolean} rethrow If false, do not re-throw the exception.
* Only used for window.onerror to not cause an infinite loop of
* rethrowing.
*/
function
report
(
ex
,
rethrow
)
{
var
args
=
_slice
.
call
(
arguments
,
1
);
if
(
lastExceptionStack
)
{
if
(
lastException
===
ex
)
{
return
;
// already caught by an inner catch block, ignore
}
else
{
processLastException
();
}
}
var
stack
=
TraceKit
.
computeStackTrace
(
ex
);
lastExceptionStack
=
stack
;
lastException
=
ex
;
lastArgs
=
args
;
// If the stack trace is incomplete, wait for 2 seconds for
// slow slow IE to see if onerror occurs or not before reporting
// this exception; otherwise, we will end up with an incomplete
// stack trace
window
.
setTimeout
(
function
()
{
if
(
lastException
===
ex
)
{
processLastException
();
}
},
(
stack
.
incomplete
?
2000
:
0
));
if
(
rethrow
!==
false
)
{
throw
ex
;
// re-throw to propagate to the top level (and cause window.onerror)
}
}
report
.
subscribe
=
subscribe
;
report
.
unsubscribe
=
unsubscribe
;
report
.
uninstall
=
unsubscribeAll
;
return
report
;
}());
/**
* TraceKit.computeStackTrace: cross-browser stack traces in JavaScript
*
* Syntax:
* s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below)
* Returns:
* s.name - exception name
* s.message - exception message
* s.stack[i].url - JavaScript or HTML file URL
* s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work)
* s.stack[i].args - arguments passed to the function, if known
* s.stack[i].line - line number, if known
* s.stack[i].column - column number, if known
* s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line#
*
* Supports:
* - Firefox: full stack trace with line numbers and unreliable column
* number on top frame
* - Opera 10: full stack trace with line and column numbers
* - Opera 9-: full stack trace with line numbers
* - Chrome: full stack trace with line and column numbers
* - Safari: line and column number for the topmost stacktrace element
* only
* - IE: no line numbers whatsoever
*
* Tries to guess names of anonymous functions by looking for assignments
* in the source code. In IE and Safari, we have to guess source file names
* by searching for function bodies inside all page scripts. This will not
* work for scripts that are loaded cross-domain.
* Here be dragons: some function names may be guessed incorrectly, and
* duplicate functions may be mismatched.
*
* TraceKit.computeStackTrace should only be used for tracing purposes.
* Logging of unhandled exceptions should be done with TraceKit.report,
* which builds on top of TraceKit.computeStackTrace and provides better
* IE support by utilizing the window.onerror event to retrieve information
* about the top of the stack.
*
* Note: In IE and Safari, no stack trace is recorded on the Error object,
* so computeStackTrace instead walks its *own* chain of callers.
* This means that:
* * in Safari, some methods may be missing from the stack trace;
* * in IE, the topmost function in the stack trace will always be the
* caller of computeStackTrace.
*
* This is okay for tracing (because you are likely to be calling
* computeStackTrace from the function you want to be the topmost element
* of the stack trace anyway), but not okay for logging unhandled
* exceptions (because your catch block will likely be far away from the
* inner function that actually caused the exception).
*
*/
TraceKit
.
computeStackTrace
=
(
function
computeStackTraceWrapper
()
{
var
sourceCache
=
{};
/**
* Attempts to retrieve source code via XMLHttpRequest, which is used
* to look up anonymous function names.
* @param {string} url URL of source code.
* @return {string} Source contents.
*/
function
loadSource
(
url
)
{
if
(
!
TraceKit
.
remoteFetching
)
{
//Only attempt request if remoteFetching is on.
return
''
;
}
try
{
var
getXHR
=
function
()
{
try
{
return
new
window
.
XMLHttpRequest
();
}
catch
(
e
)
{
// explicitly bubble up the exception if not found
return
new
window
.
ActiveXObject
(
'
Microsoft.XMLHTTP
'
);
}
};
var
request
=
getXHR
();
request
.
open
(
'
GET
'
,
url
,
false
);
request
.
send
(
''
);
return
request
.
responseText
;
}
catch
(
e
)
{
return
''
;
}
}
/**
* Retrieves source code from the source code cache.
* @param {string} url URL of source code.
* @return {Array.<string>} Source contents.
*/
function
getSource
(
url
)
{
if
(
!
isString
(
url
))
return
[];
if
(
!
hasKey
(
sourceCache
,
url
))
{
// URL needs to be able to fetched within the acceptable domain. Otherwise,
// cross-domain errors will be triggered.
var
source
=
''
;
var
domain
=
''
;
try
{
domain
=
document
.
domain
;
}
catch
(
e
)
{}
if
(
url
.
indexOf
(
domain
)
!==
-
1
)
{
source
=
loadSource
(
url
);
}
sourceCache
[
url
]
=
source
?
source
.
split
(
'
\n
'
)
:
[];
}
return
sourceCache
[
url
];
}
/**
* Tries to use an externally loaded copy of source code to determine
* the name of a function by looking at the name of the variable it was
* assigned to, if any.
* @param {string} url URL of source code.
* @param {(string|number)} lineNo Line number in source code.
* @return {string} The function name, if discoverable.
*/
function
guessFunctionName
(
url
,
lineNo
)
{
var
reFunctionArgNames
=
/function
([^
(
]
*
)\(([^
)
]
*
)\)
/
,
reGuessFunction
=
/
[
'"
]?([
0-9A-Za-z$_
]
+
)[
'"
]?\s
*
[
:=
]\s
*
(
function|eval|new Function
)
/
,
line
=
''
,
maxLines
=
10
,
source
=
getSource
(
url
),
m
;
if
(
!
source
.
length
)
{
return
UNKNOWN_FUNCTION
;
}
// Walk backwards from the first line in the function until we find the line which
// matches the pattern above, which is the function definition
for
(
var
i
=
0
;
i
<
maxLines
;
++
i
)
{
line
=
source
[
lineNo
-
i
]
+
line
;
if
(
!
isUndefined
(
line
))
{
if
((
m
=
reGuessFunction
.
exec
(
line
)))
{
return
m
[
1
];
}
else
if
((
m
=
reFunctionArgNames
.
exec
(
line
)))
{
return
m
[
1
];
}
}
}
return
UNKNOWN_FUNCTION
;
}
/**
* Retrieves the surrounding lines from where an exception occurred.
* @param {string} url URL of source code.
* @param {(string|number)} line Line number in source code to centre
* around for context.
* @return {?Array.<string>} Lines of source code.
*/
function
gatherContext
(
url
,
line
)
{
var
source
=
getSource
(
url
);
if
(
!
source
.
length
)
{
return
null
;
}
var
context
=
[],
// linesBefore & linesAfter are inclusive with the offending line.
// if linesOfContext is even, there will be one extra line
// *before* the offending line.
linesBefore
=
Math
.
floor
(
TraceKit
.
linesOfContext
/
2
),
// Add one extra line if linesOfContext is odd
linesAfter
=
linesBefore
+
(
TraceKit
.
linesOfContext
%
2
),
start
=
Math
.
max
(
0
,
line
-
linesBefore
-
1
),
end
=
Math
.
min
(
source
.
length
,
line
+
linesAfter
-
1
);
line
-=
1
;
// convert to 0-based index
for
(
var
i
=
start
;
i
<
end
;
++
i
)
{
if
(
!
isUndefined
(
source
[
i
]))
{
context
.
push
(
source
[
i
]);
}
}
return
context
.
length
>
0
?
context
:
null
;
}
/**
* Escapes special characters, except for whitespace, in a string to be
* used inside a regular expression as a string literal.
* @param {string} text The string.
* @return {string} The escaped string literal.
*/
function
escapeRegExp
(
text
)
{
return
text
.
replace
(
/
[\-\[\]
{}()*+?.,
\\\^
$|#
]
/g
,
'
\\
$&
'
);
}
/**
* Escapes special characters in a string to be used inside a regular
* expression as a string literal. Also ensures that HTML entities will
* be matched the same as their literal friends.
* @param {string} body The string.
* @return {string} The escaped string.
*/
function
escapeCodeAsRegExpForMatchingInsideHTML
(
body
)
{
return
escapeRegExp
(
body
).
replace
(
'
<
'
,
'
(?:<|<)
'
).
replace
(
'
>
'
,
'
(?:>|>)
'
).
replace
(
'
&
'
,
'
(?:&|&)
'
).
replace
(
'
"
'
,
'
(?:"|")
'
).
replace
(
/
\s
+/g
,
'
\\
s+
'
);
}
/**
* Determines where a code fragment occurs in the source code.
* @param {RegExp} re The function definition.
* @param {Array.<string>} urls A list of URLs to search.
* @return {?Object.<string, (string|number)>} An object containing
* the url, line, and column number of the defined function.
*/
function
findSourceInUrls
(
re
,
urls
)
{
var
source
,
m
;
for
(
var
i
=
0
,
j
=
urls
.
length
;
i
<
j
;
++
i
)
{
// console.log('searching', urls[i]);
if
((
source
=
getSource
(
urls
[
i
])).
length
)
{
source
=
source
.
join
(
'
\n
'
);
if
((
m
=
re
.
exec
(
source
)))
{
// console.log('Found function in ' + urls[i]);
return
{
'
url
'
:
urls
[
i
],
'
line
'
:
source
.
substring
(
0
,
m
.
index
).
split
(
'
\n
'
).
length
,
'
column
'
:
m
.
index
-
source
.
lastIndexOf
(
'
\n
'
,
m
.
index
)
-
1
};
}
}
}
// console.log('no match');
return
null
;
}
/**
* Determines at which column a code fragment occurs on a line of the
* source code.
* @param {string} fragment The code fragment.
* @param {string} url The URL to search.
* @param {(string|number)} line The line number to examine.
* @return {?number} The column number.
*/
function
findSourceInLine
(
fragment
,
url
,
line
)
{
var
source
=
getSource
(
url
),
re
=
new
RegExp
(
'
\\
b
'
+
escapeRegExp
(
fragment
)
+
'
\\
b
'
),
m
;
line
-=
1
;
if
(
source
&&
source
.
length
>
line
&&
(
m
=
re
.
exec
(
source
[
line
])))
{
return
m
.
index
;
}
return
null
;
}
/**
* Determines where a function was defined within the source code.
* @param {(Function|string)} func A function reference or serialized
* function definition.
* @return {?Object.<string, (string|number)>} An object containing
* the url, line, and column number of the defined function.
*/
function
findSourceByFunctionBody
(
func
)
{
if
(
typeof
document
===
'
undefined
'
)
return
;
var
urls
=
[
window
.
location
.
href
],
scripts
=
document
.
getElementsByTagName
(
'
script
'
),
body
,
code
=
''
+
func
,
codeRE
=
/^function
(?:\s
+
([\w
$
]
+
))?\s
*
\(([\w\s
,
]
*
)\)\s
*
\{\s
*
(\S[\s\S]
*
\S)\s
*
\}\s
*$/
,
eventRE
=
/^function on
([\w
$
]
+
)\s
*
\(
event
\)\s
*
\{\s
*
(\S[\s\S]
*
\S)\s
*
\}\s
*$/
,
re
,
parts
,
result
;
for
(
var
i
=
0
;
i
<
scripts
.
length
;
++
i
)
{
var
script
=
scripts
[
i
];
if
(
script
.
src
)
{
urls
.
push
(
script
.
src
);
}
}
if
(
!
(
parts
=
codeRE
.
exec
(
code
)))
{
re
=
new
RegExp
(
escapeRegExp
(
code
).
replace
(
/
\s
+/g
,
'
\\
s+
'
));
}
// not sure if this is really necessary, but I don’t have a test
// corpus large enough to confirm that and it was in the original.
else
{
var
name
=
parts
[
1
]
?
'
\\
s+
'
+
parts
[
1
]
:
''
,
args
=
parts
[
2
].
split
(
'
,
'
).
join
(
'
\\
s*,
\\
s*
'
);
body
=
escapeRegExp
(
parts
[
3
]).
replace
(
/;$/
,
'
;?
'
);
// semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+');
re
=
new
RegExp
(
'
function
'
+
name
+
'
\\
s*
\\
(
\\
s*
'
+
args
+
'
\\
s*
\\
)
\\
s*{
\\
s*
'
+
body
+
'
\\
s*}
'
);
}
// look for a normal function definition
if
((
result
=
findSourceInUrls
(
re
,
urls
)))
{
return
result
;
}
// look for an old-school event handler function
if
((
parts
=
eventRE
.
exec
(
code
)))
{
var
event
=
parts
[
1
];
body
=
escapeCodeAsRegExpForMatchingInsideHTML
(
parts
[
2
]);
// look for a function defined in HTML as an onXXX handler
re
=
new
RegExp
(
'
on
'
+
event
+
'
=[
\\\'
"]
\\
s*
'
+
body
+
'
\\
s*[
\\\'
"]
'
,
'
i
'
);
if
((
result
=
findSourceInUrls
(
re
,
urls
[
0
])))
{
return
result
;
}
// look for ???
re
=
new
RegExp
(
body
);
if
((
result
=
findSourceInUrls
(
re
,
urls
)))
{
return
result
;
}
}
return
null
;
}
// Contents of Exception in various browsers.
//
// SAFARI:
// ex.message = Can't find variable: qq
// ex.line = 59
// ex.sourceId = 580238192
// ex.sourceURL = http://...
// ex.expressionBeginOffset = 96
// ex.expressionCaretOffset = 98
// ex.expressionEndOffset = 98
// ex.name = ReferenceError
//
// FIREFOX:
// ex.message = qq is not defined
// ex.fileName = http://...
// ex.lineNumber = 59
// ex.columnNumber = 69
// ex.stack = ...stack trace... (see the example below)
// ex.name = ReferenceError
//
// CHROME:
// ex.message = qq is not defined
// ex.name = ReferenceError
// ex.type = not_defined
// ex.arguments = ['aa']
// ex.stack = ...stack trace...
//
// INTERNET EXPLORER:
// ex.message = ...
// ex.name = ReferenceError
//
// OPERA:
// ex.message = ...message... (see the example below)
// ex.name = ReferenceError
// ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message)
// ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace'
/**
* Computes stack trace information from the stack property.
* Chrome and Gecko use this property.
* @param {Error} ex
* @return {?Object.<string, *>} Stack trace information.
*/
function
computeStackTraceFromStackProp
(
ex
)
{
if
(
isUndefined
(
ex
.
stack
)
||
!
ex
.
stack
)
return
;
var
chrome
=
/^
\s
*at
(
.*
?)
?\(((?:
file|https
?
|blob|chrome-extension|native|eval|<anonymous>
)
.*
?)(?:
:
(\d
+
))?(?:
:
(\d
+
))?\)?\s
*$/i
,
gecko
=
/^
\s
*
(
.*
?)(?:\((
.*
?)\))?(?:
^|@
)((?:
file|https
?
|blob|chrome|
\[
native
)
.*
?)(?:
:
(\d
+
))?(?:
:
(\d
+
))?\s
*$/i
,
winjs
=
/^
\s
*at
(?:((?:\[
object object
\])?
.+
)
)?\(?((?:
ms-appx|https
?
|blob
)
:.*
?)
:
(\d
+
)(?:
:
(\d
+
))?\)?\s
*$/i
,
lines
=
ex
.
stack
.
split
(
'
\n
'
),
stack
=
[],
parts
,
element
,
reference
=
/^
(
.*
)
is undefined$/
.
exec
(
ex
.
message
);
for
(
var
i
=
0
,
j
=
lines
.
length
;
i
<
j
;
++
i
)
{
if
((
parts
=
chrome
.
exec
(
lines
[
i
])))
{
var
isNative
=
parts
[
2
]
&&
parts
[
2
].
indexOf
(
'
native
'
)
!==
-
1
;
element
=
{
'
url
'
:
!
isNative
?
parts
[
2
]
:
null
,
'
func
'
:
parts
[
1
]
||
UNKNOWN_FUNCTION
,
'
args
'
:
isNative
?
[
parts
[
2
]]
:
[],
'
line
'
:
parts
[
3
]
?
+
parts
[
3
]
:
null
,
'
column
'
:
parts
[
4
]
?
+
parts
[
4
]
:
null
};
}
else
if
(
parts
=
winjs
.
exec
(
lines
[
i
])
)
{
element
=
{
'
url
'
:
parts
[
2
],
'
func
'
:
parts
[
1
]
||
UNKNOWN_FUNCTION
,
'
args
'
:
[],
'
line
'
:
+
parts
[
3
],
'
column
'
:
parts
[
4
]
?
+
parts
[
4
]
:
null
};
}
else
if
((
parts
=
gecko
.
exec
(
lines
[
i
])))
{
element
=
{
'
url
'
:
parts
[
3
],
'
func
'
:
parts
[
1
]
||
UNKNOWN_FUNCTION
,
'
args
'
:
parts
[
2
]
?
parts
[
2
].
split
(
'
,
'
)
:
[],
'
line
'
:
parts
[
4
]
?
+
parts
[
4
]
:
null
,
'
column
'
:
parts
[
5
]
?
+
parts
[
5
]
:
null
};
}
else
{
continue
;
}
if
(
!
element
.
func
&&
element
.
line
)
{
element
.
func
=
guessFunctionName
(
element
.
url
,
element
.
line
);
}
if
(
element
.
line
)
{
element
.
context
=
gatherContext
(
element
.
url
,
element
.
line
);
}
stack
.
push
(
element
);
}
if
(
!
stack
.
length
)
{
return
null
;
}
if
(
stack
[
0
].
line
&&
!
stack
[
0
].
column
&&
reference
)
{
stack
[
0
].
column
=
findSourceInLine
(
reference
[
1
],
stack
[
0
].
url
,
stack
[
0
].
line
);
}
else
if
(
!
stack
[
0
].
column
&&
!
isUndefined
(
ex
.
columnNumber
))
{
// FireFox uses this awesome columnNumber property for its top frame
// Also note, Firefox's column number is 0-based and everything else expects 1-based,
// so adding 1
stack
[
0
].
column
=
ex
.
columnNumber
+
1
;
}
return
{
'
name
'
:
ex
.
name
,
'
message
'
:
ex
.
message
,
'
url
'
:
getLocationHref
(),
'
stack
'
:
stack
};
}
/**
* Computes stack trace information from the stacktrace property.
* Opera 10 uses this property.
* @param {Error} ex
* @return {?Object.<string, *>} Stack trace information.
*/
function
computeStackTraceFromStacktraceProp
(
ex
)
{
// Access and store the stacktrace property before doing ANYTHING
// else to it because Opera is not very good at providing it
// reliably in other circumstances.
var
stacktrace
=
ex
.
stacktrace
;
if
(
isUndefined
(
ex
.
stacktrace
)
||
!
ex
.
stacktrace
)
return
;
var
opera10Regex
=
/ line
(\d
+
)
.*script
(?:
in
)?(\S
+
)(?:
: in function
(\S
+
))?
$/i
,
opera11Regex
=
/ line
(\d
+
)
, column
(\d
+
)\s
*
(?:
in
(?:
<anonymous function:
([^
>
]
+
)
>|
([^\)]
+
))\((
.*
)\))?
in
(
.*
)
:
\s
*$/i
,
lines
=
stacktrace
.
split
(
'
\n
'
),
stack
=
[],
parts
;
for
(
var
line
=
0
;
line
<
lines
.
length
;
line
+=
2
)
{
var
element
=
null
;
if
((
parts
=
opera10Regex
.
exec
(
lines
[
line
])))
{
element
=
{
'
url
'
:
parts
[
2
],
'
line
'
:
+
parts
[
1
],
'
column
'
:
null
,
'
func
'
:
parts
[
3
],
'
args
'
:[]
};
}
else
if
((
parts
=
opera11Regex
.
exec
(
lines
[
line
])))
{
element
=
{
'
url
'
:
parts
[
6
],
'
line
'
:
+
parts
[
1
],
'
column
'
:
+
parts
[
2
],
'
func
'
:
parts
[
3
]
||
parts
[
4
],
'
args
'
:
parts
[
5
]
?
parts
[
5
].
split
(
'
,
'
)
:
[]
};
}
if
(
element
)
{
if
(
!
element
.
func
&&
element
.
line
)
{
element
.
func
=
guessFunctionName
(
element
.
url
,
element
.
line
);
}
if
(
element
.
line
)
{
try
{
element
.
context
=
gatherContext
(
element
.
url
,
element
.
line
);
}
catch
(
exc
)
{}
}
if
(
!
element
.
context
)
{
element
.
context
=
[
lines
[
line
+
1
]];
}
stack
.
push
(
element
);
}
}
if
(
!
stack
.
length
)
{
return
null
;
}
return
{
'
name
'
:
ex
.
name
,
'
message
'
:
ex
.
message
,
'
url
'
:
getLocationHref
(),
'
stack
'
:
stack
};
}
/**
* NOT TESTED.
* Computes stack trace information from an error message that includes
* the stack trace.
* Opera 9 and earlier use this method if the option to show stack
* traces is turned on in opera:config.
* @param {Error} ex
* @return {?Object.<string, *>} Stack information.
*/
function
computeStackTraceFromOperaMultiLineMessage
(
ex
)
{
// Opera includes a stack trace into the exception message. An example is:
//
// Statement on line 3: Undefined variable: undefinedFunc
// Backtrace:
// Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz
// undefinedFunc(a);
// Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy
// zzz(x, y, z);
// Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx
// yyy(a, a, a);
// Line 1 of function script
// try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); }
// ...
var
lines
=
ex
.
message
.
split
(
'
\n
'
);
if
(
lines
.
length
<
4
)
{
return
null
;
}
var
lineRE1
=
/^
\s
*Line
(\d
+
)
of linked script
((?:
file|https
?
|blob
)\S
+
)(?:
: in function
(\S
+
))?\s
*$/i
,
lineRE2
=
/^
\s
*Line
(\d
+
)
of inline#
(\d
+
)
script in
((?:
file|https
?
|blob
)\S
+
)(?:
: in function
(\S
+
))?\s
*$/i
,
lineRE3
=
/^
\s
*Line
(\d
+
)
of function script
\s
*$/i
,
stack
=
[],
scripts
=
document
.
getElementsByTagName
(
'
script
'
),
inlineScriptBlocks
=
[],
parts
;
for
(
var
s
in
scripts
)
{
if
(
hasKey
(
scripts
,
s
)
&&
!
scripts
[
s
].
src
)
{
inlineScriptBlocks
.
push
(
scripts
[
s
]);
}
}
for
(
var
line
=
2
;
line
<
lines
.
length
;
line
+=
2
)
{
var
item
=
null
;
if
((
parts
=
lineRE1
.
exec
(
lines
[
line
])))
{
item
=
{
'
url
'
:
parts
[
2
],
'
func
'
:
parts
[
3
],
'
args
'
:
[],
'
line
'
:
+
parts
[
1
],
'
column
'
:
null
};
}
else
if
((
parts
=
lineRE2
.
exec
(
lines
[
line
])))
{
item
=
{
'
url
'
:
parts
[
3
],
'
func
'
:
parts
[
4
],
'
args
'
:
[],
'
line
'
:
+
parts
[
1
],
'
column
'
:
null
// TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number.
};
var
relativeLine
=
(
+
parts
[
1
]);
// relative to the start of the <SCRIPT> block
var
script
=
inlineScriptBlocks
[
parts
[
2
]
-
1
];
if
(
script
)
{
var
source
=
getSource
(
item
.
url
);
if
(
source
)
{
source
=
source
.
join
(
'
\n
'
);
var
pos
=
source
.
indexOf
(
script
.
innerText
);
if
(
pos
>=
0
)
{
item
.
line
=
relativeLine
+
source
.
substring
(
0
,
pos
).
split
(
'
\n
'
).
length
;
}
}
}
}
else
if
((
parts
=
lineRE3
.
exec
(
lines
[
line
])))
{
var
url
=
window
.
location
.
href
.
replace
(
/#.*$/
,
''
);
var
re
=
new
RegExp
(
escapeCodeAsRegExpForMatchingInsideHTML
(
lines
[
line
+
1
]));
var
src
=
findSourceInUrls
(
re
,
[
url
]);
item
=
{
'
url
'
:
url
,
'
func
'
:
''
,
'
args
'
:
[],
'
line
'
:
src
?
src
.
line
:
parts
[
1
],
'
column
'
:
null
};
}
if
(
item
)
{
if
(
!
item
.
func
)
{
item
.
func
=
guessFunctionName
(
item
.
url
,
item
.
line
);
}
var
context
=
gatherContext
(
item
.
url
,
item
.
line
);
var
midline
=
(
context
?
context
[
Math
.
floor
(
context
.
length
/
2
)]
:
null
);
if
(
context
&&
midline
.
replace
(
/^
\s
*/
,
''
)
===
lines
[
line
+
1
].
replace
(
/^
\s
*/
,
''
))
{
item
.
context
=
context
;
}
else
{
// if (context) alert("Context mismatch. Correct midline:\n" + lines[i+1] + "\n\nMidline:\n" + midline + "\n\nContext:\n" + context.join("\n") + "\n\nURL:\n" + item.url);
item
.
context
=
[
lines
[
line
+
1
]];
}
stack
.
push
(
item
);
}
}
if
(
!
stack
.
length
)
{
return
null
;
// could not parse multiline exception message as Opera stack trace
}
return
{
'
name
'
:
ex
.
name
,
'
message
'
:
lines
[
0
],
'
url
'
:
getLocationHref
(),
'
stack
'
:
stack
};
}
/**
* Adds information about the first frame to incomplete stack traces.
* Safari and IE require this to get complete data on the first frame.
* @param {Object.<string, *>} stackInfo Stack trace information from
* one of the compute* methods.
* @param {string} url The URL of the script that caused an error.
* @param {(number|string)} lineNo The line number of the script that
* caused an error.
* @param {string=} message The error generated by the browser, which
* hopefully contains the name of the object that caused the error.
* @return {boolean} Whether or not the stack information was
* augmented.
*/
function
augmentStackTraceWithInitialElement
(
stackInfo
,
url
,
lineNo
,
message
)
{
var
initial
=
{
'
url
'
:
url
,
'
line
'
:
lineNo
};
if
(
initial
.
url
&&
initial
.
line
)
{
stackInfo
.
incomplete
=
false
;
if
(
!
initial
.
func
)
{
initial
.
func
=
guessFunctionName
(
initial
.
url
,
initial
.
line
);
}
if
(
!
initial
.
context
)
{
initial
.
context
=
gatherContext
(
initial
.
url
,
initial
.
line
);
}
var
reference
=
/ '
([^
'
]
+
)
' /
.
exec
(
message
);
if
(
reference
)
{
initial
.
column
=
findSourceInLine
(
reference
[
1
],
initial
.
url
,
initial
.
line
);
}
if
(
stackInfo
.
stack
.
length
>
0
)
{
if
(
stackInfo
.
stack
[
0
].
url
===
initial
.
url
)
{
if
(
stackInfo
.
stack
[
0
].
line
===
initial
.
line
)
{
return
false
;
// already in stack trace
}
else
if
(
!
stackInfo
.
stack
[
0
].
line
&&
stackInfo
.
stack
[
0
].
func
===
initial
.
func
)
{
stackInfo
.
stack
[
0
].
line
=
initial
.
line
;
stackInfo
.
stack
[
0
].
context
=
initial
.
context
;
return
false
;
}
}
}
stackInfo
.
stack
.
unshift
(
initial
);
stackInfo
.
partial
=
true
;
return
true
;
}
else
{
stackInfo
.
incomplete
=
true
;
}
return
false
;
}
/**
* Computes stack trace information by walking the arguments.caller
* chain at the time the exception occurred. This will cause earlier
* frames to be missed but is the only way to get any stack trace in
* Safari and IE. The top frame is restored by
* {@link augmentStackTraceWithInitialElement}.
* @param {Error} ex
* @return {?Object.<string, *>} Stack trace information.
*/
function
computeStackTraceByWalkingCallerChain
(
ex
,
depth
)
{
var
functionName
=
/function
\s
+
([
_$a-zA-Z
\x
A0-
\u
FFFF
][
_$a-zA-Z0-9
\x
A0-
\u
FFFF
]
*
)?\s
*
\(
/i
,
stack
=
[],
funcs
=
{},
recursion
=
false
,
parts
,
item
,
source
;
for
(
var
curr
=
computeStackTraceByWalkingCallerChain
.
caller
;
curr
&&
!
recursion
;
curr
=
curr
.
caller
)
{
if
(
curr
===
computeStackTrace
||
curr
===
TraceKit
.
report
)
{
// console.log('skipping internal function');
continue
;
}
item
=
{
'
url
'
:
null
,
'
func
'
:
UNKNOWN_FUNCTION
,
'
line
'
:
null
,
'
column
'
:
null
};
if
(
curr
.
name
)
{
item
.
func
=
curr
.
name
;
}
else
if
((
parts
=
functionName
.
exec
(
curr
.
toString
())))
{
item
.
func
=
parts
[
1
];
}
if
(
typeof
item
.
func
===
'
undefined
'
)
{
try
{
item
.
func
=
parts
.
input
.
substring
(
0
,
parts
.
input
.
indexOf
(
'
{
'
));
}
catch
(
e
)
{
}
}
if
((
source
=
findSourceByFunctionBody
(
curr
)))
{
item
.
url
=
source
.
url
;
item
.
line
=
source
.
line
;
if
(
item
.
func
===
UNKNOWN_FUNCTION
)
{
item
.
func
=
guessFunctionName
(
item
.
url
,
item
.
line
);
}
var
reference
=
/ '
([^
'
]
+
)
' /
.
exec
(
ex
.
message
||
ex
.
description
);
if
(
reference
)
{
item
.
column
=
findSourceInLine
(
reference
[
1
],
source
.
url
,
source
.
line
);
}
}
if
(
funcs
[
''
+
curr
])
{
recursion
=
true
;
}
else
{
funcs
[
''
+
curr
]
=
true
;
}
stack
.
push
(
item
);
}
if
(
depth
)
{
// console.log('depth is ' + depth);
// console.log('stack is ' + stack.length);
stack
.
splice
(
0
,
depth
);
}
var
result
=
{
'
name
'
:
ex
.
name
,
'
message
'
:
ex
.
message
,
'
url
'
:
getLocationHref
(),
'
stack
'
:
stack
};
augmentStackTraceWithInitialElement
(
result
,
ex
.
sourceURL
||
ex
.
fileName
,
ex
.
line
||
ex
.
lineNumber
,
ex
.
message
||
ex
.
description
);
return
result
;
}
/**
* Computes a stack trace for an exception.
* @param {Error} ex
* @param {(string|number)=} depth
*/
function
computeStackTrace
(
ex
,
depth
)
{
var
stack
=
null
;
depth
=
(
depth
==
null
?
0
:
+
depth
);
try
{
// This must be tried first because Opera 10 *destroys*
// its stacktrace property if you try to access the stack
// property first!!
stack
=
computeStackTraceFromStacktraceProp
(
ex
);
if
(
stack
)
{
return
stack
;
}
}
catch
(
e
)
{
if
(
TraceKit
.
debug
)
{
throw
e
;
}
}
try
{
stack
=
computeStackTraceFromStackProp
(
ex
);
if
(
stack
)
{
return
stack
;
}
}
catch
(
e
)
{
if
(
TraceKit
.
debug
)
{
throw
e
;
}
}
try
{
stack
=
computeStackTraceFromOperaMultiLineMessage
(
ex
);
if
(
stack
)
{
return
stack
;
}
}
catch
(
e
)
{
if
(
TraceKit
.
debug
)
{
throw
e
;
}
}
try
{
stack
=
computeStackTraceByWalkingCallerChain
(
ex
,
depth
+
1
);
if
(
stack
)
{
return
stack
;
}
}
catch
(
e
)
{
if
(
TraceKit
.
debug
)
{
throw
e
;
}
}
return
{
'
name
'
:
ex
.
name
,
'
message
'
:
ex
.
message
,
'
url
'
:
getLocationHref
()
};
}
computeStackTrace
.
augmentStackTraceWithInitialElement
=
augmentStackTraceWithInitialElement
;
computeStackTrace
.
computeStackTraceFromStackProp
=
computeStackTraceFromStackProp
;
computeStackTrace
.
guessFunctionName
=
guessFunctionName
;
computeStackTrace
.
gatherContext
=
gatherContext
;
return
computeStackTrace
;
}());
module
.
exports
=
TraceKit
;
},{
"
4
"
:
4
}]},{},[
3
])(
3
)
});
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment